将多个选择表达式组合到动态类中

问题描述 投票:0回答:1

我想创建一个允许单独的程序集(一个插件)在运行时向对象添加属性的系统。插件是插件,因此可以随时添加/删除。

基础实体

public class FooDto 
{
    public int Id { get; set; }
    public string Description { get; set; }
    ...
}

这些数据直接来自使用带有EF Core的linq投影的数据库,看起来像这样:

DbContext.Foos.Select(foo => new FooDto {
    Id = foo.Id,
    Description = foo.Description,
    ...
});

插件

插件可以创建以前不存在的新表+关系。假设有一个插件创建了一个名为“ Bar”的表,我们想将Bar的Description添加到FooDto。

public class PluginFoo : Foo
{
    public Bar Bar { get; set; }
}

它会在某处定义一个选择表达式:

pluginFoo => new PluginFooDto {
    BarDescription = pluginFoo.Bar.Description
}

我可以做到,每个插件都完全独立运行,并会触发其自己的数据库查询,但我想尝试将它们全部合并为一个查询。

基本上,插件的表达式将唯一共享的是,表达式参数将是相同的基类。使用的实际类可能是派生类,其中将包含插件使用的其他数据(如PluginFoo所示)。

从理论上讲,可以创建将基本Select表达式和新的select表达式组合在一起的SQL语句。

这是我的问题,是否可以实际创建这样的系统?


我的思考过程:

  • 而不是执行选择,而是使用自定义扩展方法'ProjectToAndExtend'或在其中挂接所有其他数据的方法。
  • 基本DTO将实现一个接口,该接口具有名为'ExtendedValues'的属性。这可以是IDictionary / object / dynamic。
  • 每个插件都会定义一个返回'Expression>'的方法]
  • 在'ProjectToAndExtend'中,我将浏览每个插件表达式并获取所有属性/投影,并创建一个包含所有插件表达式组合的运行时(proxy?)类。
  • 然后我将创建一个新表达式,将使用所有提供的插件投影的组合将其投影到运行时类上。
  • 然后将这个新表达式添加到'ExtendedValues'属性的原始选择中。

现在,我对表达式构建和运行时类创建的知识是有限的。我主要想知道是否有可能这样做?

[如果可能的话,我不希望有人给我一个实际的例子。我只是想避免花费数小时来学习有关Expressions / Reflection的信息。Emit只是想知道这种系统不可能完全解决。

如果有人对如何执行此操作确实有更好/不同的想法,我很想听听。

提前感谢!


编辑

为方便起见,我们假设数据库已经为所有插件提供了完整的架构。问题不在于修改架构的步骤,而是与查询数据有关。

我认为查看项目如何查询其数据可能会有所帮助,因此这里是一个如何设置DbContext的示例。每个插件都有各自独立的上下文,仅处理其使用的数据。

[基础项目

public class BaseDbContext : DbContext 
{
    public DbSet<Foo> Foos { get; set; }
}

插件项目

public class PluginDbContext : DbContext 
{
    public DbSet<PluginFoo> Foos { get; set; }
}

[2 DbContext指向完全相同的数据库和表,但是它们在模式上具有不同的作用域(Foo不了解Bar,但是PluginFoo知道)。

对于我的问题;在数据库中所有模式正确的情况下,是否可以将Bar的数据附加到DbSet<foo>的选择表达式中?

如果使用Ef Core无法实现,那么可以直接使用Linq-Sql吗?

c# entity-framework linq dynamic reflection.emit
1个回答
0
投票

TL; DR可以,但是我永远不建议实际做这样的事情。它过于复杂且无法管理。它还需要一堆运行时类型生成+表达式构建。

[我正在回答这个问题,以防有人发现其中的一部分有助于解决不相关的问题。

解决方案

一个大问题是因为DbSet是一个类而不是一个接口。这意味着,如果您尝试向使用不属于DbSet类型的属性的选择添加表达式,则不会生成sql。

为了解决这个问题,我们需要在运行时使用TypeBuilder生成一堆类型,并且仅通过接口进行查询。

基本思想是:

  • 对于每种DbSet类型,创建一个新的运行时类型,其中包含您将需要查询的所有属性。
  • 为先前创建的每种类型创建一个具有DbSet的运行时dbContext。
  • 每次您要查询数据库时,请使用接口而不是直接访问DbSet(基本上是存储库模式)。

实施细节

运行时数据库集类型

为了能够为DbSet生成运行时类型,我定义了一个基本类型,如下所示:

public class Foo
{
    public int Id { get; set; }
    public string Description { get; set; }
}

然后任何想要扩展此类型的插件都将使用如下接口:

public interface IFooPlugin
{
   int BarId { get; set; }
   Bar Bar { get; set; }
}

然后,您需要某种方式将IFooPlugin链接到Foo。如果这样做,则可以创建一个从Foo继承并实现IFooPlugin的运行时类型。您想为插件做接口,以便您在创建的类型中实现多个插件。

您的最终动态类型将如下所示:

public class DynamicFoo : IFooPlugin
{
    public int Id { get; set; }
    public string Description { get; set; }
    public int BarId { get; set; }
    public Bar Bar { get; set; }
}

查询动态类型

在您的控制器中,您将无法使用任何类进行查询,因为您需要可以从接口获得的协方差。如果将DbSet<DynamicFoo>强制转换为IQueryable<Foo>,将很高兴生成使用DynamicFoo上任何属性的sql。如果您尝试在DbSet<Foo>上执行相同的操作,则会遇到问题。

因此,从本质上讲,您可能希望具有一堆IRepository<T>的IDbContext,其中IRepository<T>本质上是DbSet的包装。

添加动态数据

现在,最后一个困难的部分是将动态数据实际添加到Sql投影中。而不是执行“选择”,而是创建一个名为“ SelectAndExtend”的新扩展方法。在这里,我们将找到要添加的所有其他表达式。

您需要定义某种返回Expression<Func<TSource, TResult>>的接口。 TSource可以是FooIFooPlugin,也可以是最终动态DbSet类型实现的任何类型。 TResult实际上可以是任何类。

对于这些Expression<Func<TSource, TResult>>中的每一个,您都希望获得它们“选择”的所有属性。使用此功能,您将要创建具有所有这些属性的另一种运行时类型。这将成为动态属性的DTO。

然后,您可以使用ExpressionVisitor构建新的表达式,然后复制所有表达式。最初,我使IQueryable实际返回具有所有必需属性的新运行时类型,但是如果您使用Task的方法执行异步方法,则会导致问题。

相反,我在原始DTO中添加了一个额外的属性,称为'ExtensionProperties',它只是一个对象。然后,可以选择直接在此新的“ ExtensionProperties”属性中生成的动态DTO。

结果

最终这确实有效,但是在执行类似操作时存在很多问题。我能够得到它来生成单个Sql语句,该语句使用在运行时定义的类型上的属性。然后,我可以添加/删除Dll以更改从服务器返回的对象。

我已经琐碎了很多东西,并跳过了一些其他问题,但是基本上,这并不是一件值得做的事情。请记住,这仅处理查询插件数据,实际上能够提交插件数据将是完全不同的野兽。

我从来没有真正完成它,因为我满足了我对是否可能的好奇心,并且还远远地意识到这将不再是我想要使用的东西。

如果有人有任何疑问,请告诉我。

© www.soinside.com 2019 - 2024. All rights reserved.