EF, Code First - Как установить индивидуальное значение идентификатора Guid для вставки

Я сталкиваюсь со следующей проблемой при работе с добавлением новых сущностей в БД с Guid в качестве первичных ключей - EF 5 с использованием метода Code first.

Я знаю, что есть много похожих тем, в которых я часами занимался этой проблемой, но я не мог найти тему с этой проблемой.

В качестве примера мой класс POCO:

public class EntityRole : IAdminModel
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public Guid Id { get; set; }

    [Required]
    [MaxLength(50)]
    public string Name { get; set; }

    [Required]
    [Display(Name = "Role code")]
    [MaxLength(20)]
    public string RoleCode { get; set; }

    [Display(Name = "Entities Assigned")]
    [InverseProperty("Role")]
    public List<Entity> Entities { get; set; }
}

RoleCode и Name - это только текстовые данные, которые можно редактировать в панели администратора, поэтому не учитывайте эти имена полей.

При добавлении нового объекта я не указываю значения первичного ключа. Пока все хорошо, все работает отлично. В качестве поля первичного ключа, которое требует значения и имеет автоматическое генерирование значений, предполагается, что он не всегда должен указывать идентификатор, но если я устанавливаю значение, его следует сохранить (если включена вставка идентификатора).

Но в некоторых случаях я хочу указать значение первичного ключа, например, для начального посева моей БД со значениями. Мне нужно это для последующей обработки - давайте просто скажем, что мне нужен этот конкретный Гид. Поэтому, если у меня есть класс конфигурации:

// Initial data seed
protected override void Seed(WebPortalDbContext context)
{
    context.MenuItems.AddOrUpdate(
        m => m.Id,
        new EntityRole {Id = new Guid("268bf332-910e-4ea1-92f8-1ac0611f4c62"), Name = "Some name", RoleCode = "SN"},
    );
}

Настройка ключа Guid не работает, даже если я регулярно добавляю:

using (var context = new MyDBContext())
{
    context.MenuItems.Add(
        new Entity() {Id = new Guid("<some guid>"), Name = "fooname" /*some other values*/}
    );

    context.SaveChanges();
}

Что я имею в трассировке SQL Server:

exec sp_executesql N'declare @generated_keys table([Id] uniqueidentifier)
insert [dbo].[EntityRoles]([Name], [RoleCode])
output inserted.[Id] into @generated_keys
values (@0, @1)
select t.[Id]
from @generated_keys as g join [dbo].[EntityRoles] as t on g.[Id] = t.[Id]
where @@ROWCOUNT > 0',N'@0 nvarchar(50),@1 nvarchar(20)',@0=N'Chief Captain',@1=N'CO1'

Здесь очевидно, что новое значение Guid просто не отправляется из генератора EF SQL на SQL Server, поэтому проблема в EF.

Итак, я удалил атрибут DatabaseGeneratedOption.Identity, тогда это нормально, но я теряю автогенерацию ключа Id, что не работает для меня, так как это очень редкий случай.

Единственное решение от меня сейчас:

Мне пришлось перезаписать метод SaveChanges() в DBContext и изменить все сущности, у которых есть состояние, которое нужно добавить (я понял эту идею из здесь):

/// <summary> Custom processing when saving entities in changetracker </summary>
public override int SaveChanges()
{
    // recommended to explicitly set New Guid for appropriate entities -- http://msdn.microsoft.com/en-us/library/dd283139.aspx
    foreach (var entry in ChangeTracker.Entries().Where(e => e.State == EntityState.Added))
    {
        var t = entry.Entity.GetType();
        if (t.GetProperty("Id") == null)
            continue;

        var info = t.GetProperty("Id").GetCustomAttributes(typeof (DatabaseGeneratedAttribute), true).Cast<DatabaseGeneratedAttribute>();
        if (!info.Any() || info.Single().DatabaseGeneratedOption != DatabaseGeneratedOption.Identity)
        {
            if (t.GetProperty("Id").PropertyType == typeof(Guid) && (Guid)t.GetProperty("Id").GetValue(entry.Entity, null) == Guid.Empty)
                t.GetProperty("Id").SetValue(entry.Entity, Guid.NewGuid(), null);
        }
    }
    return base.SaveChanges();
}

В сочетании с этим необходимо удалить все DatabaseGeneratedOption. Все модели имеют первичные ключи с именем "Id", следуя одной из лучших тем практики для соглашений об именах.

Но это не выглядит очень изящным workaronud, потому что я thnk EF5 должен иметь возможность обрабатывать такие случаи. Он работает с идентификаторами Int, если включена вставка идентификатора.

У кого-то есть идея, как добиться лучшего решения проблемы?

Ответ 1

Простой способ сделать это, поскольку это GUID, состоит в том, чтобы иметь один конструктор в вашем классе, например

public EntityRole()
{
   Id = Guid.NewGuid();
}

и удалите параметр сгенерированный базой данных или измените его на DatabaseGeneratedOption.None.

Ответ 2

Вы можете пометить Id как виртуальное свойство. Поэтому в проекте, где вы создаете поддельные данные, вы можете иметь внутренний EntityRole с overrideen Id, и здесь вы можете удалить атрибут DatabaseGeneratedOption.Identity или изменить его на DatabaseGeneratedOption.None.

Ответ 3

Visual Studio 2017, С#, EntityFramework v6.0 позволяет переопределять пользовательское значение, передавая значение при создании через контроллер. его не нужно определять в модели, поскольку он уже определен в ApplicationUser.

private string DateGuid()
    {
        string pre = "Ugly Users 😍🤔🤣😂-";
        return pre + Guid.NewGuid();
    }

// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]    
public async Task<ActionResult> Register(RegisterViewModel model)
    {
        if (ModelState.IsValid)
        {
            var user = new ApplicationUser { Id = DateGuid() };
            var result = await UserManager.CreateAsync(user, model.Password);
...