实体框架 + ODATA + 动态列映射

Posted

技术标签:

【中文标题】实体框架 + ODATA + 动态列映射【英文标题】:Entity Framework + ODATA + Dynamic column mapping 【发布时间】:2021-06-01 06:31:13 【问题描述】:

我有两个 OData 控制器:

PersonsController
PersonsUnrestrictedController

它们不同的唯一方式是几个属性必须根据控制器从persons表中的不同列获取它们的值。

PersonsController 将发回 Person 列表,其中 Person 的名字、姓氏等是别名,而 PersonsUnrestrictedController 将发回带有这些人的真实姓名的 Person 列表。所有其他属性将完全相同,包括导航属性及其与其他表的关系。

PersonsController 在任何情况下都不能透露一个人的真实姓名,这一点非常重要。

是否可以动态切换:

[Column("AltGivenName")]
public string GivenName  get; set; 

[Column("GivenName")]
public string GivenName  get; set; 

取决于控制器?

或者有两个属性 GivenName 和 AltGivenName 并根据控制器动态隐藏/显示其中 1 个:

[DataMember(Name="GivenName")] //Either should this one be ignored
public string AltGivenName  get; set; 

public string GivenName  get; set;  //or this one, depending on controller

或者还有其他可能的解决方法吗?

编辑:添加我的代码

Startup.cs

    public class Startup
    
        public Startup(IConfiguration configuration)
        
            Configuration = configuration;
        

        public IConfiguration Configuration  get; 

        public void ConfigureServices(IServiceCollection services)
        
            services.AddDbContext<PersonContext>(options =>  options.UseSqlServer(Configuration.GetConnectionString("MyConnection")); );
            services.AddOData();
        

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        
            if (env.IsDevelopment())
            
                app.UseDeveloperExceptionPage();
            

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            
                endpoints.MapODataRoute("odata", "odata", GetEdmModel());
                endpoints.Select().Expand().MaxTop(null).Count();
            );
        

        private static IEdmModel GetEdmModel()
        
            var builder = new ODataConventionModelBuilder();
            var persons = builder.EntitySet<Person>("Persons");
            return builder.GetEdmModel();
        
    

PersonContext.cs

    public class PersonContext : DbContext
    
        public DbSet<DbPerson> Persons  get; set; 

        public PersonContext(DbContextOptions<PersonContext> options) : base(options)
        
        
    

DbPerson.cs

    [Table("Person")]
    public class DbPerson
    
        [Key]
        public int Id  get; set; 

        public string GivenName  get; set; 

        public string AltGivenName  get; set; 
    

Person.cs

    public class Person
    
        [Key]
        public int Id  get; set; 

        public string GivenName  get; set; 
    

MappingHelper.cs

    public static class MappingHelper
    
        public static Person ToPerson(this DbPerson dbPerson)
        
            return new Person
            
                Id = dbPerson.Id,
                GivenName = dbPerson.GivenName,
            ;
        

        public static Person ToAnonymousPerson(this DbPerson dbPerson)
        
            return new Person
            
                Id = dbPerson.Id,
                GivenName = dbPerson.AltGivenName,
            ;
        
    

PersonsController.cs

    public class PersonsController : ODataController
    
        private readonly PersonContext _context;

        public PersonsController(PersonContext context)
        
            _context = context;
        

        [EnableQuery]
        public IActionResult Get()
        
            return new ObjectResult(_context.Persons.Select(MappingHelper.ToPerson));
        
    

运行以下查询需要 5-10 秒 http://localhost:4871/odata/persons?$top=10

如果我改变:

            return new ObjectResult(_context.Persons.Select(MappingHelper.ToPerson));

            return new ObjectResult(_context.Persons);

改变

var persons = builder.EntitySet<Person>("Persons"); 

var persons = builder.EntitySet<DbPerson>("Persons");

相同的查询需要 50-100 毫秒

person 表中有大约 150k 人。

【问题讨论】:

请显示两个控制器之间的一些示例 url,例如,您是否希望使用两条不同的路由,或者是否有其他机制来确定这是受限制还是不受限制的查询?您的示例只有“/Persons”路线。 您还可以展示您希望每个控制器提供的一两个其他功能或操作,以帮助展示您的需求的动态性质。 您希望在PersonsController 上支持哪些操作?为什么它需要是一个完全独立的控制器? 【参考方案1】:

配置 PersonsUnrestrictedController 以返回标准的 DB 操作集,这实际上是您的内部 DbPerson api,并将 PersonsController 定义为专用控制器,用于访问名为的数据传输对象Person.

您已经定义了大部分元素,我们需要更改的只是控制器实现。

以下内容没有变化:

DbPerson.cs Person.cs PersonContext.cs

EdmModel 中定义两个控制器:

private static IEdmModel GetEdmModel()

    var builder = new ODataConventionModelBuilder();
    var persons = builder.EntitySet<Person>("Persons");
    var unrestricted = builder.EntitySet<DbPerson>("PersonsUnrestricted");
    return builder.GetEdmModel();

与其创建一个 Mapping 方法来映射对象,更简单的模式是在控制器中添加一个方法来提供 base 查询,all 该控制器中的操作应该使用。

通过这种方式,您可以强制执行通用过滤条件、包含或排序,而无需在每个操作中声明相同的查询。它是一种更容易长期维护的模式,当您有 20 个操作或函数都具有需要重构的相同查询时,或者当您必须跨多个控制器重构类似条件​​时,您会感谢我。

公共人员控制者:

public class PersonsController : ODataController

    private readonly PersonContext _context;

    public PersonsController(PersonContext context)
    
        _context = context;
    
    
    private IQueryable<Person> GetQuery() 
    
        return from p in _context.Persons
               select new Person 
                
                   Id = p.Id,
                   GivenName = p.AltGivenName 
               ;
    

    [EnableQuery]
    public IActionResult Get()
    
        return Ok(GetQuery());
    

    [EnableQuery]
    public IActionResult Get(int key)
    
        return Ok(GetQuery().Single(x => x.Id == key));
    

不受限制的控制器:

public class PersonsUnrestrictedController : ODataController

    private readonly PersonContext _context;

    public PersonsUnrestrictedController(PersonContext context)
    
        _context = context;
    

    private IQueryable<DbPerson> GetQuery() 
    
        return _context.Persons;
    

    [EnableQuery]
    public IActionResult Get()
    
        return Ok(GetQuery());
    

    [EnableQuery]
    public IActionResult Get(int key)
    
        return Ok(GetQuery().Single(x => x.Id == key));
    

【讨论】:

OP 指出此解决方案在 asp.net 和 EF6 中不起作用,并导致“无法在 linq to entity 查询中构造实体或复杂类型”这是多年前讨论的关于 SO:***.com/a/5325861/1690217 一种解决方法是在数据库模式中实现一个视图,但这对我来说似乎有点矫枉过正。【参考方案2】:

这里的其他答案特别关注您对映射到同一个表的 2 个单独控制器的请求,但听起来您真正需要的是来自 OData 实体的自定义只读提要,它仍然具有查询支持。

在 OData 中,这通常通过在返回一组可查询 DTO 的标准控制器上定义 Function 端点来实现。

以下内容没有变化:

DbPerson.cs Person.cs PersonContext.cs

然而,在这个解决方案中,我们将有一个PersonsController,它具有标准的Get() 端点和一个函数 View() 端点。

private static IEdmModel GetEdmModel()

    var builder = new ODataConventionModelBuilder();
    var persons = builder.EntitySet<DbPerson>("Persons");
    persons.EntityType.Collection.Function("View").ReturnsCollection<Person>();
    return builder.GetEdmModel();

PersonsController.cs

public class PersonsController : ODataController

    private readonly PersonContext _context;

    public PersonsController(PersonContext context)
    
        _context = context;
    
    
    [HttpGet]
    [EnableQuery]
    public IActionResult Get()
    
        return Ok(_context.Persons);
    

    [HttpGet]
    [EnableQuery]
    public IActionResult View()
    
        return Ok(from p in _context.Persons
                  select new Person 
                   
                      Id = p.Id,
                      GivenName = p.AltGivenName 
                  );
     


OP 已专门询问过asp.net-core,但此回复适用于EF6asp.netasp.net-core

【讨论】:

非常感谢克里斯。这几乎正​​是我想要的。与我真正想要的唯一不同的是 Persons 和 PersonsUnrestricted 的 View-type 方法,所以我仍然需要两个控制器(因为实际上我想做一些映射在这两种情况下还有其他属性/字段。)但是我的最后一个问题是,获得这种映射功能的唯一方法是否是使用函数?没有办法直接在Get方法中做到这一点,是吗? 没关系,我以为我已经测试过了,但显然我上次一定犯了一些错误,因为当我再次尝试时,它现在起作用了。唯一的区别是我需要在不使用函数时在我的业务模型中定义一个键。再次,非常感谢您的所有帮助。我学到了一些对我很有帮助的新东西。我已经为此苦苦挣扎了一段时间。【参考方案3】:

在这种情况下(事实上,作为一般规则),我建议您将数据库模型与业务模型分开,并在两者之间设置一些映射逻辑。

这样您就可以拥有两个业务模型PersonAnonymousPerson 和一个数据库模型DbPerson(随意称呼它们)。然后实现一些映射逻辑,将DbPerson 转换为Person,将DbPerson 转换为AnonymousPerson,并根据您所处的情况使用适当的映射逻辑。

例如,查看this fiddle。有关更多详细信息,请考虑我们有以下数据库模型和两个业务对象模型:

public class DbPerson

    public string GivenName  get; set; 
    public string FamilyName  get; set; 
    public string AltGivenName  get; set; 


public class Person

    public string GivenName  get; set; 
    public string FamilyName  get; set; 


public class AnonymousPerson

    public string Nickname  get; set; 

然后我们需要一些逻辑来将数据库模型转换为两个业务对象中的任何一个。我在这里使用extension methods,但如果您愿意,也可以使用普通方法:

public static class MappingHelper

    public static Person ToPerson(this DbPerson dbPerson)
    
        return new Person
        
            GivenName = dbPerson.GivenName,
            FamilyName = dbPerson.FamilyName
        ;
    
    
    
    public static AnonymousPerson ToAnonymousPerson(this DbPerson dbPerson)
    
        return new AnonymousPerson
        
            Nickname = dbPerson.AltGivenName
        ;
    

现在,要以正确的格式获取数据,您只需使用所需的映射方法(使用类似实体框架的方法):

// From PersonsController (or a service beneath)
var persons = myDatabase.Persons.Select(MappingHelper.ToPerson);
// Same as                       Select(p => p.ToPerson())
// or                            Select(p => MappingHelper.ToPerson(p))


// And from PersonsUnrestrictedController
var anonPersons = myDatabase.Persons.Select(MappingHelper.ToAnonymousPerson);

【讨论】:

非常感谢您的回答。如果您确实有具体示例,将不胜感激。 补充一下,我有点担心映射所有数据会大大降低服务的性能。 @Marty78 添加了示例。我希望它有帮助:) @Marty78 我相信像这样的数据映射是一种非常常见的方法,因为它有助于将业务逻辑与低级数据存储模型分开。 C# 在实例化对象方面进行了非常优化,因此除非您对性能有极高 要求或需要一些非常繁重 的映射逻辑,否则您不会注意到任何变化有了这个。 不幸的是,这似乎对我不起作用。我尝试了你的方法(完全按照你写的那样做)只是映射了一些属性,它已经从 50-100 毫秒到 5-10 秒。

以上是关于实体框架 + ODATA + 动态列映射的主要内容,如果未能解决你的问题,请参考以下文章

使用 ASP.NET Core OData 8.0 映射动态 OData 路由

在实体框架中获取属性的映射列名称

CsvHelper 动态列映射

带有 Firebird 的实体框架引发动态 SQL 错误

Azure Synapse 映射数据流 - 派生列中列名的动态映射

是否可以将表动态映射到实体框架核心或任何其他 ORM