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



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

基础实体

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所示)。

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

我的问题来了,有可能真正创建这样的系统吗


我的思考过程:

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

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

如果可能的话,我不希望有人真的给我一个这样的工作示例。我只是想避免花数小时学习Expressions/Reflection.Emit,只是为了发现这种系统是不可能的。

如果有人对如何做到这一点有任何更好/不同的想法,我会很乐意倾听。

提前感谢!


编辑

为了解决这个问题,让我们假设数据库已经为所有插件提供了一个完整的模式。问题不在于修改模式的步骤,而在于查询数据。

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

基地项目

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>中的select表达式中?

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

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

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

解决方案

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

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

基本思想是:

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

实现详细信息

运行时数据库集类型

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

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投影中。与其执行"Select",不如创建一个名为"SelectAndExtend"之类的新扩展方法。这就是我们将要添加的所有额外表达式的位置。

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

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

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

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

结果

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

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

我从来没有真正完成过它,因为我满足了我对它是否可能的好奇心,也意识到它不会是我未来想要使用的东西。

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

最新更新