将多个 Select 表达式组合成动态 class

Combining multiple Select Expressions into dynamic class

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

基础实体

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,
    ...
});

插件

插件可以创建以前不存在的新 tables + 关系。假设有一个插件创建了一个名为 'Bar' 的 table,我们想将 Bar 的描述添加到 FooDto。

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

它会在某处定义一个像这样的 select 表达式:

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

我可以让每个插件完全独立运行并触发自己的数据库查询,但我想尝试将它们全部组合成 1 个查询。

基本上插件的表达式唯一共享的是表达式参数将具有相同的基础class。实际使用的 class 可能是派生的 class,它将包含插件使用的附加数据(如 PluginFoo 所示)。

理论上可以创建一个 SQL 语句,它结合了基础 Select 和新的 select 表达式。

我的问题来了,真的可以创建这样的系统吗?


我的思考过程:

现在我对表达式构建和运行时 class 创建的了解有限。我主要想知道这样的事情是否可能?

如果可能的话,我不希望任何人实际给我一个工作示例。我只是想避免花几个小时学习 Expressions/Reflection。发送只是为了发现这种系统是不可能的。

如果有人对如何执行此操作有任何 better/different 想法,我会很乐意倾听。

提前致谢!


编辑:

为了这个问题,我们假设数据库已经有所有插件的完整架构。这个问题不是关于修改架构的步骤,而是关于查询数据。

我认为了解项目如何查询它们的数据可能会有所帮助,所以这里有一个如何设置 DbContext 的示例。每个插件都有自己独立的上下文,只处理它使用的数据。

基地项目

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

插件项目中

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

2 个 DbContext 指向完全相同的数据库和 tables,但它们在架构上具有不同的范围(Foo 不知道 Bar,但 PluginFoo 知道).

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

如果 Ef Core 做不到,是否可以直接使用 Linq-Sql?

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

我正在回答这个问题,以防有人发现其中的某个部分对不相关的问题有帮助。

解决方案

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

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

基本思路是:

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

实施细节

运行时 DbSet 类型

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

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 的 运行time 类型。您希望为插件创建接口,以便在您创建的类型中实现多个插件。

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

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

查询动态类型

在您的控制器中,您将无法使用任何 class 进行查询,因为您需要可以从接口获得的协方差。如果您将 DbSet<DynamicFoo> 转换为 IQueryable<Foo>,那么生成使用 DynamicFoo 上的任何属性的 sql 将是非常愉快的。如果你试图在 DbSet<Foo> 上做同样的事情,你会遇到问题。

所以基本上你可能想要一个 IDbContext 有一堆 IRepository<T>,其中 IRepository<T> 本质上是 DbSet 的包装器。

添加动态数据

现在最后一个困难的部分是将动态数据实际添加到您的 Sql 投影中。而不是做 'Select',而是创建一个名为 'SelectAndExtend' 之类的新扩展方法。这是我们将找到所有要添加的额外表达式的地方。

您需要某种定义了 return 和 Expression<Func<TSource, TResult>> 的接口。 TSource 可以是 FooIFooPlugin,或由最终动态 DbSet 类型实现的任何类型。 TResult 可以是任何 class.

对于这些 Expression<Func<TSource, TResult>> 中的每一个,您都需要获得它们 'select into' 的所有属性。使用它,您将想要创建另一个具有所有这些属性的 运行time 类型。这将成为动态属性的 DTO。

然后您可以使用 ExpressionVisitor 构建一个新表达式并复制所有表达式。最初我使 IQueryable 实际上 return 具有所有必需属性的新 运行time 类型,但是如果您执行异步方法,这会导致问题,因为 Task's.

相反,我向名为 'ExtensionProperties' 的原始 DTO 添加了一个额外的 属性,它只是一个对象。然后我可以 select 我生成的动态 DTO 直接进入这个新的 'ExtensionProperties' 属性。

结果

最后这确实有效,但是这样做有很多问题。我能够让它生成单个 Sql 语句,该语句使用在 运行 时定义的类型的属性。然后我可以 add/remove Dll 来更改从服务器获取 returned 的对象。

我已经轻视了一堆东西并跳过了一些其他问题,但基本上它并不是真正值得做的事情。请记住,这仅涉及查询插件数据,实际上能够提交插件数据将是一个完全不同的野兽。

我从来没有真正完全完成它,因为我满足了我对它是否可能的好奇心,并且已经足够了解它不会成为我想要在未来使用的东西。

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