Substitute entity class at runtime in Entity Framework Core

c# domain-driven-design entity-framework-core

Question

We are following Domain Driven Design principles in our model by combining data and behavior in entity and value object classes. We often need to customize behavior for our customers. Here's a simple example where a customer wants to change the way FullName is formatted:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    // Standard format is Last, First
    public virtual string FullName => $"{LastName}, {FirstName}";
}

public class PersonCustom : Person
{
    // Customer wants to see First Last instead
    public override string FullName => $"{FirstName} {LastName}";
}

The DbSet is configured on the DbContext as you would expect:

public virtual DbSet<Person> People { get; set; }

At runtime, the PersonCustom class needs to be instantiated in place of the Person class. This is not a problem when our code is responsible for instantiating the entity, such as when adding a new person. The IoC Container/Factory can be configured to substitute the PersonCustom class for the Person class. However, when querying data, EF is responsible for instantiating the entity. When querying context.People, is there some way to configure or intercept the entity creation to substitute PersonCustom for the Person class at runtime?

I've used inheritance above, but I could have implemented an IPerson interface instead if it makes a difference. Either way, you can assume the interface will be the same in both classes.

Edit: A little more info about how this is deployed... The Person class would be part of the standard build that goes to all customers. PersonCustom would go into a separate custom assembly that only goes to the customer that wants the change, so they would get the standard build plus the custom assembly. We would not create a separate build of the entire project to accomodate the customization.

1
0
9/15/2018 8:11:22 PM

Accepted Answer

I have had a similar task in my project as well. There are interceptor ways to do this but have two issues:

  1. The object will get out of sync with the proxy
  2. This is not architecturally sound way to do things.

So I ended up converting objects to required types using AutoMapper. There are also other libraries out there that can do the same thing. This way in each domain you can easily get the required object.

This may not be what you asked but I hope this'll help you.

1
9/15/2018 5:23:01 PM

Popular Answer

Based on everything you provided, I believe creating another DbContext for the other customer will best fit your scenario.

Below code shows a test where I used PersonContext to add a Person entity. Then use PersonCustomContext to read the same entity but is instantiated as a PersonCustom.

Another point of interest is the explicit configuration of the PersonCustom key to point to the PersonId key defined in its base type Person.

using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using Xunit;

public class Tests
{
    [Fact]
    public void PersonCustomContext_can_get_PersonCustom()
    {
        var connection = new SqliteConnection("datasource=:memory:");
        connection.Open();

        var options = new DbContextOptionsBuilder()
            .UseSqlite(connection)
            .Options;

        using (var ctx = new PersonContext(options))
            ctx.Database.EnsureCreated();

        using (var ctx = new PersonContext(options))
        {
            ctx.Add(new Person { FirstName = "John", LastName = "Doe" });
            ctx.SaveChanges();
        }

        using (var ctx = new PersonCustomContext(options))
        {
            Assert.Equal("John Doe", ctx.People.Single().FullName);
        }
    }
}
public class PersonContext : DbContext
{
    public PersonContext(DbContextOptions options) : base(options) { }
    public DbSet<Person> People { get; set; }
}
public class PersonCustomContext : DbContext
{
    public PersonCustomContext(DbContextOptions options) : base(options) { }
    public DbSet<PersonCustom> People { get; set; }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<PersonCustom>(builder =>
        {
            builder.HasKey(p => p.PersonId);
        });
    }
}
public class Person
{
    public int PersonId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public virtual string FullName => $"{LastName}, {FirstName}";
}
public class PersonCustom : Person
{
    public override string FullName => $"{FirstName} {LastName}";
}

Caveat: for some reason, the test fails using the in-memory provider. So you might want to consider that if you or your customer uses in-memory for testing. It might be a bug on EF Core itself since it's working perfectly fine with sqlite inmemory as shown.



Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow