Natural identifiers in your domain (with Entity Framework Core) - generated by database

1 minute read

Part 2 is about configuring EF Core with databse side generated identifiers, but there is also a part 1 where a configuration for server side generated identifiers is explained

This is a continuation of this article

How to configure it in Entity Framework Core?

Described solution is working fine on EF Core 3.0

Configuration of Order class in EF Core:

public class OrderId {
  public long Value { get; private set; }
}

public class Order {
  public OrderId Id { get; private set; }
  [...]
}

public class OrderConfiguration : IEntityTypeConfiguration<Order> {
  public void Configure(EntityTypeBuilder<Order> builder)
  {
    builder.ToTable(nameof(Order));
    builder.HasKey(order => order.Id);
        
    builder.Property(order => order.Id)
          .ValueGeneratedOnAdd();
  }
}

CREATE TABLE [dbo].[Order] (
[Id] [bigint] IDENTITY(1,1) NOT NULL,
[...]
)

There are few things that needs to be done before EF Core understands how to handle our identities correctly:

  1. Identifiers have to implement following methods / interfaces:
    1. Equals / GetHashCode
    2. IComparable
    3. == / != operators
  2. Custom implementation for IValueConverterSelector and ValueConverter<TIdentifier, TType> with converter mapping hints that returns temporary identifiers and allows EF Core to track entities
  3. Replacing internal EF Core implementation of IValueConverterSelector with our custom implementation

After all those changes EF Core will be able to understand LinQ queries with our identities and translates it correctly to SQL queries (will be evaluated on SQL with WHERE clause).

public class OrderService : IOrderService {
  public Order GetOrder(OrderId id) {
    return dbContext.Orders.SingleOrDefault(c => c.Id == id);
  }
}

EF Core migrations

To bring back EF Core migrations with support for natural identifiers you need to do this:

  1. Implement custom UseIdentityColumn method and store annotation for SqlServerValueGenerationStrategy.IdentityColumn under custom name to avoid EF Core type validations from Microsoft.EntityFrameworkCore.SqlServerPropertyExtensions.CheckValueGenerationStrategy
  2. Override SqlServerMigrationsAnnotationProvider and read your custom annotation from point above, when you find them, add an annotation for property with name SqlServerAnnotationNames.Identity

After those points, every time when you will generate new migrations by EF Core mechanism, it will generate correct annotations and allows you to use database as a source for values of ID, e.g.:

migrationBuilder.CreateTable(
    name: "Order",
    columns: table => new
    {
        Id = table.Column<long>(nullable: false)
            .Annotation("SqlServer:Identity", "1, 1"),
        Description = table.Column<string>(nullable: true),
        Customer_FirstName = table.Column<string>(nullable: true),
        Customer_LastName = table.Column<string>(nullable: true)
    },
    constraints: table =>
    {
        table.PrimaryKey("PK_Order", x => x.Id);
    });

Code with examples could be found on my GitHub