我想创建一个允许单独的程序集(一个插件)在运行时向对象添加属性的系统。插件是插件,因此可以随时添加/删除。
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语句。
这是我的问题,是否可以实际创建这样的系统?
现在,我对表达式构建和运行时类创建的知识是有限的。我主要想知道是否有可能这样做?
[如果可能的话,我不希望有人给我一个实际的例子。我只是想避免花费数小时来学习有关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吗?
TL; DR可以,但是我永远不建议实际做这样的事情。它过于复杂且无法管理。它还需要一堆运行时类型生成+表达式构建。
[我正在回答这个问题,以防有人发现其中的一部分有助于解决不相关的问题。
一个大问题是因为DbSet是一个类而不是一个接口。这意味着,如果您尝试向使用不属于DbSet类型的属性的选择添加表达式,则不会生成sql。
为了解决这个问题,我们需要在运行时使用TypeBuilder
生成一堆类型,并且仅通过接口进行查询。
基本思想是:
运行时数据库集类型
为了能够为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可以是Foo
或IFooPlugin
,也可以是最终动态DbSet类型实现的任何类型。 TResult实际上可以是任何类。
对于这些Expression<Func<TSource, TResult>>
中的每一个,您都希望获得它们“选择”的所有属性。使用此功能,您将要创建具有所有这些属性的另一种运行时类型。这将成为动态属性的DTO。
然后,您可以使用ExpressionVisitor构建新的表达式,然后复制所有表达式。最初,我使IQueryable实际返回具有所有必需属性的新运行时类型,但是如果您使用Task的方法执行异步方法,则会导致问题。
相反,我在原始DTO中添加了一个额外的属性,称为'ExtensionProperties',它只是一个对象。然后,可以选择直接在此新的“ ExtensionProperties”属性中生成的动态DTO。
最终这确实有效,但是在执行类似操作时存在很多问题。我能够得到它来生成单个Sql语句,该语句使用在运行时定义的类型上的属性。然后,我可以添加/删除Dll以更改从服务器返回的对象。
我已经琐碎了很多东西,并跳过了一些其他问题,但是基本上,这并不是一件值得做的事情。请记住,这仅处理查询插件数据,实际上能够提交插件数据将是完全不同的野兽。
我从来没有真正完成它,因为我满足了我对是否可能的好奇心,并且还远远地意识到这将不再是我想要使用的东西。
如果有人有任何疑问,请告诉我。