在运行时在Entity Framework Core中替换实体类

时间:2018-09-15 17:12:28

标签: c# entity-framework-core domain-driven-design

我们通过在实体和值对象类中组合数据和行为来遵循模型中的域驱动设计原则。我们经常需要为客户定制行为。这是一个简单的示例,其中客户想要更改FullName的格式化方式:

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}";
}

如您所愿,在DbContext上配置了DbSet:

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

在运行时,需要实例化PersonCustom类来代替Person类。当我们的代码负责实例化实体时,例如添加新人员时,这不是问题。可以将IoC容器/工厂配置为将PersonCustom类替换为Person类。但是,查询数据时,EF负责实例化实体。查询context.People时,是否有某种方法可以配置或拦截实体创建以在运行时将PersonCustom替换为Person类?

我在上面使用了继承,但是如果有区别的话,我可以实现IPerson接口。无论哪种方式,您都可以假定两个类中的接口都相同。

编辑:有关如何部署的更多信息... Person类将成为面向所有客户的标准构建的一部分。 PersonCustom将进入一个单独的自定义程序集,该程序集仅用于需要更改的客户,因此他们将获得标准版本以及自定义程序集。我们不会为整个项目创建单独的版本来适应自定义。

3 个答案:

答案 0 :(得分:1)

我在项目中也有类似的任务。有拦截器可以执行此操作,但是有两个问题:

  1. 对象将与代理不同步
  2. 从结构上讲,这不是行之有效的方法。

因此,我最终使用AutoMapper将对象转换为所需类型。还有其他可以执行相同操作的库。这样,您可以在每个域中轻松获得所需的对象。

这可能不是您的要求,但希望对您有所帮助。

答案 1 :(得分:1)

如果您的数据存储在基础实体中,我将不会尝试使用单独的类。我要做的是使用ViewModel或任何您想调用的模型,然后将您的实体映射到此实体(手动或使用其中一种AutoMapper进行映射)。通常,除了数据库交互之外,我不建议您使用实体。所有逻辑和其他操作都应在单独的类中进行,即使该类恰好是所有属性的1:1副本(如果以后进行更改比处理其他迁移或冒其他中断风险更容易处理)变化)。

一个简短的例子,可以更好地解释我的想法。

//entity
public class Person
{
    public string FirstName {get; set;}
    public string LastName {get; set;}
}

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

//customer specific ViewModel
public class CustomerSpecificModel : PersonModel
{
    // Standard format is Last, First
    public virtual string FullName => $"{FirstName} {LastName}";
}


//Mapping
public class Mapping
{
    public void DoSomeWork()
    {
        var db = new People();
        var defaultPerson = db.People.Select(p => new PersonModel
        {
            FirstName = p.FirstName,
            LastName = p.LastName
        };

        var customerSpecificModel = db.People.Select(p => new CustomerSpecificModel
        {
            FirstName = p.FirstName,
            LastName = p.LastName
        }

        Console.WriteLine(defaultPerson.First().FullName); //would return LastName, FirstName
        Console.WriteLine(customerSpecificModel.First().FullName); //would return FirstName LastName
    }   
}

答案 2 :(得分:1)

根据您提供的一切,我相信为其他客户创建另一个DbContext最适合您的情况。

下面的代码显示了一个测试,其中我使用PersonContext添加了一个Person实体。然后使用PersonCustomContext读取相同的实体,但实例化为PersonCustom

另一个关注点是PersonCustom键的显式配置,以指向以其基本类型PersonId定义的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}";
}

注意:由于某种原因,使用内存提供程序测试失败。因此,如果您或您的客户使用内存进行测试,则可能要考虑一下。如图所示,这可能是EF Core本身的一个错误,因为它可以与sqlite内存完美配合。