C# – EF, Code First – How to set a custom Guid identity value on insert

.net-4.0centity-framework

I`m facing the following problem when dealing with inserting new entities in the DB that has Guid as primary keys – EF 5 with Code first approach.

I know there are a lot similar topics as I was roving for hours for this issue, but I couldn`t find topic with this problem.

As an example, my POCO class is:

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 and Name are just text data, that is editable in the admin panel, so don`t consider those field names.

When adding new entity I don`t specify primary key values. So far so good, all works fine here. As a primary key field that requires value and has auto-generation of values, it is supposed not to always specify Id, but if I set a value, it should be kept (if identity insert is enabled).

But in some cases I want to specify a primary key value, for example for initial seeding of my DB with values. I need it for later processing – lets just say I need that specific Guid to be there. So if I have in my Configuration class:

// 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"},
    );
}

The Guid key setting goes not work even if I do a regular add:

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

    context.SaveChanges();
}

What I have in the SQL Server trace is:

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'

Here it is obvious that the new Guid value is just not sent from the EF SQL generator to the SQL Server, so the problem is in EF.

So I removed the DatabaseGeneratedOption.Identity attribute, then it is ok, but I lose the auto generation of the Id key, which does not work for me as it is very rare case.

The only solution from me for now:

I ended up is to overwrite the SaveChanges() method of the DBContext and modify all entities that have state to be added (I took the idea from here):

/// <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();
}

In combination with this, all DatabaseGeneratedOption should be removed. All models has primary keys named "Id", following one of the best practice topics for naming conventions.

But this does not looks very elegant workaronud, because I thnk EF5 should be able to handle such cases. It works for Int identities if identity insert is on.

So does someone has an idea how to achieve better solution on the problem?

Best Answer

The simple way for you to do this since it is a GUID is to have one constructor in your class like

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

and remove The database generated option or change it to DatabaseGeneratedOption.None.