简体   繁体   English

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

[英]Substitute entity class at runtime in Entity Framework Core

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: 这是一个简单的示例,客户希望更改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}";
}

The DbSet is configured on the DbContext as you would expect: 如您所料,DbSet是在DbContext上配置的:

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

At runtime, the PersonCustom class needs to be instantiated in place of the Person class. 在运行时,需要实例化PersonCustom类来代替Person类。 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. 可以将IoC容器/工厂配置为将PersonCustom类替换为Person类。 However, when querying data, EF is responsible for instantiating the entity. 但是,查询数据时,EF负责实例化实体。 When querying context.People, is there some way to configure or intercept the entity creation to substitute PersonCustom for the Person class at runtime? 查询context.People时,是否有某种方法可以配置或拦截实体创建以在运行时将PersonCustom替换为Person类?

I've used inheritance above, but I could have implemented an IPerson interface instead if it makes a difference. 我在上面使用继承,但是如果有区别的话,我可以实现IPerson接口。 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. 编辑:有关如何部署的更多信息... Person类将成为所有客户使用的标准版本的一部分。 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. PersonCustom将进入一个单独的自定义程序集,该程序集仅用于需要更改的客户,因此他们将获得标准版本以及自定义程序集。 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: 有拦截器可以执行此操作,但是有两个问题:

  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 . 因此,我最终使用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. 这可能不是您要的,但希望对您有所帮助。

I wouldn't try to use a separate class if your data is stored in the base entity regardless. 如果您的数据存储在基本实体中,我将不会尝试使用单独的类。 What I would do is use a ViewModel, or whatever you want to call it, and map (manually or with one of the variety of AutoMapper's out there) your entity to this. 我要做的是使用ViewModel或任何您想调用的模型,然后将您的实体映射到此实体(手动或使用其中一种AutoMapper进行映射)。 As a general rule, I do not recommend using your entity for anything more than database interaction. 通常,除了数据库交互之外,我不建议您使用实体。 All logic and other manipulation should happen in a separate class, even if that class happens to be a 1:1 copy of all the properties (much easier to deal with if there are changes later on than having to do additional migrations or risk other breaking changes). 所有逻辑和其他操作都应在单独的类中进行,即使该类恰好是所有属性的1:1副本(如果以后进行更改比处理其他迁移或冒其他中断风险更容易处理)变化)。

A brief example to better explain my thought. 一个简短的例子可以更好地解释我的想法。

//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
    }   
}

Based on everything you provided, I believe creating another DbContext for the other customer will best fit your scenario. 根据您提供的所有内容,我相信为其他客户创建另一个DbContext最适合您的情况。

Below code shows a test where I used PersonContext to add a Person entity. 下面的代码显示了一个测试,其中我使用PersonContext添加了Person实体。 Then use PersonCustomContext to read the same entity but is instantiated as a PersonCustom . 然后使用PersonCustomContext读取相同的实体,但实例化为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 . 另一个兴趣点是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}";
}

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. 如图所示,这可能是EF Core本身的一个错误,因为它可以与sqlite内存完美配合。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM