TL; DR
Это поведение не характерно для one-to-one
отношений, one-to-many
ведет себя точно так же.
Если вы хотите, чтобы EF генерировал исключение с вашим текущим кодом, тогда сопоставьте отдельный внешний ключ для GenericObject
modelBuilder
.Entity<GenericObject>()
.HasRequired(u => u.UserWhoGeneratedThisObject)
.WithOptional()
.Map(config =>
{
config.MapKey("UserId");
});
Фактический ответ
На самом деле отношения one-to-many
страдают точно такой же проблемой. Давайте сделаем несколько тестов.
Один-на-один
public class GenericObject
{
public int Id { get; set; }
public string Description { get; set; }
public UserObject UserWhoGeneratedThisObject { get; set; }
}
public class UserObject
{
public int Id { get; set; }
public string Name { get; set; }
}
Конфигурация
modelBuilder
.Entity<GenericObject>()
.HasRequired(u => u.UserWhoGeneratedThisObject)
.WithOptional();
В этом случае GenericObject.Id
является первичным ключом, а внешний ключ одновременно ссылается на UserObject
сущность.
Тест 1. Создать только GenericObject
var context = new AppContext();
GenericObject myObject = new GenericObject
{
Description = "some description"
};
context.GenericObjects.Add(myObject);
context.SaveChanges();
Выполненный запрос
INSERT [dbo].[GenericObjects]([Id], [Description])
VALUES (@0, @1)
-- @0: '0' (Type = Int32)
-- @1: 'some description' (Type = String, Size = -1)
Исключение
Оператор INSERT конфликтует с ограничением FOREIGN KEY "FK_dbo.GenericObjects_dbo.UserObjects_Id". Конфликт произошел в базе данных «TestProject», таблице «dbo.UserObjects», столбце «Id».
Заявление было прекращено.
Поскольку GenericObject
является единственной сущностью, EF выполняет запрос insert
, и он не выполняется, поскольку в базе данных нет UserObject
с Id
, равным 0
.
Тест 2. Создайте 1 UserObject и GenericObject
var context = new AppContext();
GenericObject myObject = new GenericObject
{
Description = "some description"
};
UserObject user = new UserObject
{
Name = "user"
};
context.UserObjects.Add(user);
context.GenericObjects.Add(myObject);
context.SaveChanges();
Выполненные запросы
INSERT [dbo].[UserObjects]([Name])
VALUES (@0)
SELECT [Id]
FROM [dbo].[UserObjects]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()
-- @0: 'user' (Type = String, Size = -1)
INSERT [dbo].[GenericObjects]([Id], [Description])
VALUES (@0, @1)
-- @0: '10' (Type = Int32)
-- @1: 'some description' (Type = String, Size = -1)
Теперь контекст содержит UserObject
(Id = 0) и GenericObject
(Id = 0). EF считает, что GenericObject
ссылается на UserObject
, потому что его внешний ключ равен 0, а UserObject
первичный ключ равен 0. Так что сначала EF вставляет UserObject
в качестве принципала и потому что он считает GenericObject
зависимым от этого. пользователь получает возвращенный UserObject.Id
и выполняет вторую вставку с ним, и все в порядке.
Тест 3. Создайте 2 UserObject и GenericObject
var context = new AppContext();
GenericObject myObject = new GenericObject
{
Description = "some description"
};
UserObject user = new UserObject
{
Name = "user"
};
UserObject user2 = new UserObject
{
Name = "user"
};
context.UserObjects.Add(user);
context.UserObjects.Add(user2);
context.GenericObjects.Add(myObject);
context.SaveChanges();
Исключение
System.Data.Entity.Infrastructure.DbUpdateException 'произошло в EntityFramework.dll
Дополнительная информация: невозможно определить основной конец отношения TestConsole.Data.GenericObject_UserWhoGeneratedThisObject. Несколько добавленных объектов могут иметь один и тот же первичный ключ.
EF видит, что есть 2 UserObject
в контексте с Id
равно 0
и GenericObject.Id
равно 0, поэтому каркас просто не может определенно соединить сущности, потому что есть несколько возможных вариантов.
Один-ко-многим
public class GenericObject
{
public int Id { get; set; }
public string Description { get; set; }
public int UserId { get; set; } //this property is essential to illistrate the problem
public UserObject UserWhoGeneratedThisObject { get; set; }
}
public class UserObject
{
public int Id { get; set; }
public string Name { get; set; }
}
Конфигурация
modelBuilder
.Entity<GenericObject>()
.HasRequired(o => o.UserWhoGeneratedThisObject)
.WithMany()
.HasForeignKey(o => o.UserId);
В этом случае GenericObject
имеет отдельный UserId
в качестве внешнего ключа, ссылающегося на UserObject
сущность.
Тест 1. Создать только GenericObject
Тот же код, что и в one-to-one
. Он выполняет тот же запрос и выдает то же исключение. Причина та же.
Тест 2. Создайте 1 UserObject и GenericObject
Тот же код, что и в one-to-one
.
Выполненные запросы
INSERT [dbo].[UserObjects]([Name])
VALUES (@0)
SELECT [Id]
FROM [dbo].[UserObjects]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()
-- @0: 'user' (Type = String, Size = -1)
-- Executing at 14.03.2019 18:52:35 +02:00
INSERT [dbo].[GenericObjects]([Description], [UserId])
VALUES (@0, @1)
SELECT [Id]
FROM [dbo].[GenericObjects]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()
-- @0: 'some description' (Type = String, Size = -1)
-- @1: '3' (Type = Int32)
Выполненные запросы очень похожи на one-to-one
test 2. Единственное отличие состоит в том, что теперь GenericObject
имеет отдельный UserId
внешний ключ. Рассуждения практически одинаковы. Контекст содержит UserObject
сущность с Id
равным 0
и GenericObject
с UserId
равными теперь, поэтому EF считает их подключенными и выполняет вставку UserObject
, затем берет UserObject.Id
и выполняет вторую вставку с ним.
Тест 3. Создайте 2 UserObject и GenericObject
Тот же код, что и в one-to-one
. Он выполняет тот же запрос и выдает то же исключение. Причина та же.
Как видно из этих тестов, проблема не связана с one-to-one
отношениями.
Но что, если мы не добавим UserId
к GenericObject
? В этом случае EF сгенерирует для нас внешний ключ UserWhoGeneratedThisObject_Id
, и теперь в базе данных есть внешний ключ, но ему не сопоставлено свойство. В этом случае каждый отдельный тест немедленно выдаст следующее исключение
Объекты в «AppContext.GenericObjects» участвуют в отношении «GenericObject_UserWhoGeneratedThisObject». 0 связанных 'GenericObject_UserWhoGeneratedThisObject_Target' были найдены. 1 'GenericObject_UserWhoGeneratedThisObject_Target' ожидается.
Почему это происходит?Теперь EF не может определить, подключены ли GenericObject
и UserObject
, поскольку в GenericObject
отсутствует свойство внешнего ключа.В этом случае EF может полагаться только на свойство навигации UserWhoGeneratedThisObject
, которое равно null
, и поэтому возникает исключение.
Это означает, что вы можете достичь этой ситуации для one-to-one
, когда в ключе есть внешний ключ.база данных, но никакое свойство не сопоставлено с ней EF выдаст то же исключение для кода в вашем вопросе.Это легко сделать, обновив конфигурацию
modelBuilder
.Entity<GenericObject>()
.HasRequired(u => u.UserWhoGeneratedThisObject)
.WithOptional()
.Map(config =>
{
config.MapKey("UserId");
});
Метод Map
указывает EF создать отдельный UserId
внешний ключ в GenericObject
.С этим изменением код в вашем вопросе вызовет исключение
using (var ctx = new TestContext())
{
GenericObject myObject = new GenericObject();
myObject.description = "testobjectdescription";
User usr = new User() { Name = "Test"};
ctx.Users.Add(usr);
//myObject.UserWhoGeneratedThisObject = usr;
ctx.GenericObjects.Add(myObject);
ctx.SaveChanges();
}
Объекты в AppContext.GenericObjects участвуют в отношении 'GenericObject_UserWhoGeneratedThisObject'.0 связанных 'GenericObject_UserWhoGeneratedThisObject_Target' были найдены.1 'GenericObject_UserWhoGeneratedThisObject_Target' ожидается.