【问题标题】:Combining multiple Select Expressions into dynamic class将多个选择表达式组合成动态类
【发布时间】:2020-03-09 01:28:59
【问题描述】:

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

基础实体

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 的描述添加到 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”中,我会查看每个插件表达式并获取所有属性/投影,并创建一个包含所有插件表达式组合的运行时(代理?)类。
  • 然后,我将创建一个新表达式,该表达式将使用所有提供的插件投影的组合投影到运行时类。
  • 然后,我会将这个新表达式添加到原始选择的“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&lt;foo&gt; 的选择表达式中?

如果 Ef Core 无法做到这一点,是否可以直接使用 Linq-Sql?

【问题讨论】:

  • 如果数据来自数据库,您希望插件如何“创建”额外的列?
  • @DavidG 每个插件都有自己的迁移,这些迁移将在安装/添加时运行。从技术上讲,当插件被删除时,这些额外的列将保留,但一旦它被卸载,数据就不再需要发送到客户端。
  • EF 有一个静态模型,因此“添加”任何内容、修改数据库都需要一些 EF 迁移过程。并且删除插件不会删除我猜的存储数据。这个问题太复杂了。在运行时创建类型是可能的,但这些类型也只能由运行时创建的代码或反射使用,如数据绑定。创建动态创建类型的 DbSet ?我认为这行不通。特别是因为数据库是持久的,但类型不是。即使是旧的 Database-First 方法,也是使用 C# 代码生成来创建 DbSet。
  • @Holger 这很可能无法做到。我认为这将取决于 Ef Core 如何使用表达式来构建 sql。我已经对问题添加了一个编辑,希望能澄清一点。我不太关心在这样的插件系统中是否可以修改数据库架构,在最坏的情况下,我们可以根据客户拥有的插件手动设置数据库架构。我最关心的是我们是否可以有通用代码来处理它的查询。
  • 作为一个想法,对于少量数据,我一直在使用 XML-Columns 来处理这种情况。它们可以在不影响数据结构的情况下进行扩展。一些用户可以在 xml 中有更多行,而另一些则更少。如果您的插件不需要整个表,您可以使用属性列表、键值对以及可以存储所有内容的通用工具让生活变得更轻松。查询编译时未知的表是相对简单的事情,但您不支持 Entire EF-Relationship /Foreignkey/Migration。您可以使用 ObjectQuery 将其读取到动态类或使用 SqlReader 读取到表。

标签: c# entity-framework linq dynamic reflection.emit


【解决方案1】:

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

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

解决方案

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

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

基本思路是:

  • 为每个 DbSet 类型创建一个新的运行时类型,其中包含您需要查询的所有属性。
  • 创建一个运行时 dbContext,它为之前创建的每种类型都有一个 DbSet。
  • 任何时候您想查询数据库都使用接口而不是直接访问 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&lt;DynamicFoo&gt; 转换为IQueryable&lt;Foo&gt;,它会非常乐意生成使用DynamicFoo 上的任何属性的sql。如果您尝试在 DbSet&lt;Foo&gt; 上执行相同操作,则会遇到问题。

因此,基本上您可能需要一个 IDbContext,其中包含一堆 IRepository&lt;T&gt;,其中 IRepository&lt;T&gt; 本质上是 DbSet 的包装器。

添加动态数据

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

您需要定义某种返回Expression&lt;Func&lt;TSource, TResult&gt;&gt; 的接口。 TSource 可以是FooIFooPlugin,或由最终动态 DbSet 类型实现的任何类型。 TResult 可以是任何类。

对于这些Expression&lt;Func&lt;TSource, TResult&gt;&gt; 中的每一个,您都需要获取它们“选择”的所有属性。使用它,您将希望创建另一个具有所有这些属性的运行时类型。这将成为动态属性的 DTO。

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

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

结果

最后这确实有效,但是做这样的事情有很多问题。我能够让它生成一个使用在运行时定义的类型的属性的单个 Sql 语句。然后我可以添加/删除 Dll 来更改从服务器返回的对象。

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

我实际上从来没有完全完成它,因为我满足了我对它是否可能的好奇心,而且还远远地意识到它不会成为我以后想要使用的东西。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-21
    相关资源
    最近更新 更多