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

时间:2021-03-02 17:15:51

标签: c# asp.net-core asp.net-web-api odata

我有两个 OData 控制器:

PersonsController
PersonsUnrestrictedController

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

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

PersonsController 在任何情况下都不得透露个人真实姓名,这一点极为重要。

是否可以动态切换:

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

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

取决于控制器?

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

[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 表中大约有 15 万人。

3 个答案:

答案 0 :(得分: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));
    }
}

答案 1 :(得分:1)

此处的其他答案专门针对您对映射到同一表的 2 个独立控制器的请求,但听起来您真正需要的是来自 OData 实体的自定义只读源,该源仍然具有查询支持。

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

以下内容没有变化:

  • DbPerson.cs
  • Person.cs
  • PersonContext.cs

但是,在此解决方案中,我们将有一个 PersonsController,它将具有标准的 Get() 端点和一个 Function 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,但此回复适用于 EF6 & asp.netasp.net-core

答案 2 :(得分:0)

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

通过这种方式,您可以拥有两个业务模型 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);