Search code examples
c#.netentity-framework-coredomain-driven-designconverters

Generic value converter for strongly typed ids in ef core 8


I'm trying to create a generic value converter for strongly typed ids in ef core so i don't create coverter for every strongly typed id that i have but i don't now how to initialize it when i get the value from the database.

Or maybe there's another way to achive this functionality, will appreciate the help

public interface EntityId {
    Guid Identifier { get; init; } 
}


public record MoveId(Guid Identifier) : EntityId;


var converter = new EntityIdConverter<MovieId>();


builder
    .Property(movie => movie.Id)
    .HasConversion(converter);


public class EntityIdConverter<TId> : ValueConverter<TId, Guid> 
    where TId : EntityId
{
    public EntityIdConverter()
        : base(
            id => id.Identifier,
            value => new EntityId(value) // How to initialize id record without using Activator or reflection?
        )
    {
    }
}

Solution

  • Erik, I missed first that you were looking for a solution without reflection. It is possible, but not straight forward.

    Generic EntityId EF Core converter

    First, you need an extra interface. It defines a static method that creates the object. I named it Create.

    public interface ICreateGuid<T, Guid>
    {
        public abstract static T Create(Guid identifier);
    }
    

    Add the interface and the static Create method to the MovieId object.

    public record MovieId(Guid Identifier) : EntityId, ICreateGuid<MovieId, Guid>
    {
        public static MovieId Create(Guid Identifier)
        {
            return new MovieId(Identifier);
        }
    }
    

    This would allow you to create the converter like this:

    public class EntityIdValueConverter<T> : ValueConverter<T, Guid> where T : EntityId, ICreateGuid<T, Guid>
    {
        public EntityIdValueConverter()
            : base(
                id => id.Identifier,
                guid=> T.Create(guid)
            )
        { }
    }
    

    Unfortunately, you will get a compiler error:

    CS8972 - A lambda expression with attributes cannot be converted to an expression tree.

    So, I got stuck there two years ago. Last year KennethHoff posted a genious workaround: https://github.com/dotnet/csharplang/discussions/5997.

    With this you can create your generic ValueConverter:

    public class EntityIdValueConverter<T> : ValueConverter<T, Guid> where T : EntityId, ICreateGuid<T, Guid>
    {
        public EntityIdValueConverter()
            : base(
                id => id.Identifier,
                value => LambdaHelper<T>.Create(value)
            )
        { }
        private class LambdaHelper<U> where U : ICreateGuid<U, Guid>
        {
            public static U Create(Guid value) => U.Create(value);
        }
    }
    

    Usage

    You can now add the converter to your entity with:

    .HasConversion(new EntityIdValueConverter<MovieId>())
    

    or in the ConfigureConventions with

    configurationBuilder.Properties<MovieId>()
        .HaveConversion<EntityIdValueConverter<MovieId>>();
    

    Apply automatically

    You can also apply the converter automatically to all derived entities in your model. For this you can write a method that is called during the EF Core model building.

    public static void ApplyEntityIdValueConverters(this ModelBuilder modelBuilder)
    {
        // List of all EF Core objects and properties that are mapped to the db
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            var properties = entityType.ClrType.GetProperties()
                .Where(p => typeof(EntityId).IsAssignableFrom(p.PropertyType));
    
            foreach (var property in properties)
            {
     
                var converterType = typeof(EntityIdValueConverter<>).MakeGenericType(property.PropertyType);
    
                var converter = Activator.CreateInstance(converterType);
                if (converter is null)
                {
                    throw new Exception("Error creating EntityIdValueConverter Object: " + entityType.ClrType.ToString() + " Property: " + property.ToString());
                }
                modelBuilder.Entity(entityType.Name).Property(property.Name)
                .HasConversion((ValueConverter)converter);
            }
        }
    }
    

    Registration

    In your DBContext class you override the OnModelCreating method and apply it.

    public class VCDbContext : DbContext
    {
    // Schema and Set definition
    
    // constructor
    
     protected override void OnModelCreating(ModelBuilder modelBuilder)
     {
         //modelBuilder.ApplyConfiguration(...
    
         modelBuilder.ApplyEntityIdConverters();
     }
    }
    

    Now all derived classes of GuidId are automatically registered with the appropriate converter on that type. There is no need to apply .HasConversion() in the model configuration or .HaveConversion() in the ConfigureConventions.

    I hope this helps you.