【问题标题】:Substitute entity class at runtime in Entity Framework Core在 Entity Framework Core 中运行时替换实体类
【发布时间】:2023-03-20 14:15:02
【问题描述】:

我们通过在实体和值对象类中结合数据和行为,在模型中遵循领域驱动设计原则。我们经常需要为我们的客户定制行为。下面是一个客户想要更改 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 Container/Factory 可以配置为用 PersonCustom 类代替 Person 类。但是,在查询数据时,EF 负责实例化实体。在查询 context.People 时,是否有某种方法可以配置或拦截实体创建以在运行时用 PersonCustom 代替 Person 类?

我在上面使用了继承,但如果它有所作为,我可以实现一个 IPerson 接口。无论哪种方式,您都可以假设两个类中的接口相同。

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

【问题讨论】:

  • 为什么不改用DbSet&lt;PersonCustom&gt; People
  • @PaoloGo 我在帖子中添加了一些部署信息。请注意,PersonCustom 类仅对需要定制的一位客户可用。您是否建议我们为该客户创建一个单独的 DbContext 以指定 DbSet&lt;PersonCustom&gt; People
  • 继承不是正确的模式。继承是关于“事物”的分层类别,定义事物。仅仅拥有另一个名称格式偏好并不会从本质上重新定义 Person 是什么。 IMO 策略是正确的模式。

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


【解决方案1】:

我的项目中也有类似的任务。有拦截器方法可以做到这一点,但有两个问题:

  1. 对象将与代理不同步
  2. 这不是架构合理的做事方式。

所以我最终使用AutoMapper 将对象转换为所需的类型。还有其他图书馆可以做同样的事情。这样在每个域中都可以轻松获取所需的对象。

这可能不是你问的,但我希望这会对你有所帮助。

【讨论】:

  • 所有建议都是有效的,但我认为这是我们场景的最佳解决方案。谢谢!
【解决方案2】:

如果您的数据存储在基础实体中,我不会尝试使用单独的类。我要做的是使用 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
    }   
}

【讨论】:

  • DDD 提倡在实体类中组合数据和行为,并远离Anemic Domain Model。我已经将实体映射到 DTO 以通过网络发送,所以我希望避免另一层映射。
【解决方案3】:

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

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

另一个有趣的地方是PersonCustom 键的显式配置,以指向在其基本类型Person 中定义的PersonId 键。

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 inmemory 完美配合,如图所示。

【讨论】:

    猜你喜欢
    • 2017-05-05
    • 2014-01-13
    • 2023-03-29
    • 2018-02-05
    • 2022-01-19
    • 2018-02-27
    • 1970-01-01
    • 2017-04-26
    • 1970-01-01
    相关资源
    最近更新 更多