12
votes

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?

3
We use EF 5 code-first, and just set the [Key] attribute on the key properties of our entities. Every time we add an entity whose key is not set, and then call context.SaveChanges(), the key is automatically generated and is available to us from the entity object. The major difference is that our keys are ints instead of Guids - not sure if that matters.Daniel Gabriel
Yes, with ints it is working fine, cause I think it is requential in the DB and the SQL Server generates new Id for itVasil Popov

3 Answers

3
votes

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.

2
votes

You can mark Id as virtual property. So in you project where you creating fake data you can have internal EntityRole with overriden Id and here you can remove DatabaseGeneratedOption.Identity attribute or change it on DatabaseGeneratedOption.None.

0
votes

Visual Studio 2017, C#, EntityFramework v6.0 allows you to override with a custom value by passing the value in on creation through your controller. it does not need to be defined in the model bc it is already defined within 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);
...