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.
I have had a similar task in my project as well. There are interceptor ways to do this but have two issues:
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.
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.