EF Core:使用多态关联序列化复合 objects

EF Core: Serializing composite objects with polymorphic associations

在我的设计中,我有一个 Challenge 聚合根,它维护一个策略列表,这些策略共同决定挑战是否完成。

有几种类型的策略以它们自己的方式检查挑战提交。每种类型的策略都继承自基础 ChallengeFullfilmentStrategy,如图所示。

提交后,我使用以下代码加载挑战及其策略:

return _dbContext.Challenges
    .Where(c => c.Id == challengeId)
    .Include(c => c.Solution)
    .Include(c => c.FulfillmentStrategies)
        .ThenInclude(s => (s as BasicMetricChecker).ClassMetricRules)
        .ThenInclude(r => r.Hint)
    .Include(c => c.FulfillmentStrategies)
        .ThenInclude(s => (s as BasicMetricChecker).MethodMetricRules)
        .ThenInclude(r => r.Hint)
    .Include(c => c.FulfillmentStrategies)
        .ThenInclude(s => (s as BasicNameChecker).Hint)
    .FirstOrDefault();

此设置是策略层次结构引入的多态性的结果。最近,我尝试添加一个更复杂的策略(ProjectChecker,在图表中标记为 red)。通过一个中间class,它引入了一个复合关系,现在一个Strategy有一个Strategies列表(通​​过SnippetStrategies class)。

此更改使数据模型严重复杂化,因为现在代码应如下所示:

return _dbContext.Challenges
    .Where(c => c.Id == challengeId)
    .Include(c => c.Solution)
    .Include(c => c.FulfillmentStrategies)
        .ThenInclude(s => (s as BasicMetricChecker).ClassMetricRules)
        .ThenInclude(r => r.Hint)
    .Include(c => c.FulfillmentStrategies)
        .ThenInclude(s => (s as BasicMetricChecker).MethodMetricRules)
        .ThenInclude(r => r.Hint)
    .Include(c => c.FulfillmentStrategies)
        .ThenInclude(s => (s as BasicNameChecker).Hint)
    .Include(c => c.FulfillmentStrategies)
        .ThenInclude(s => (s as ProjectChecker).SnippetStrategies)
        .ThenInclude(snippetStrats => snippetStrats.Strategies)
        .ThenInclude(s => (s as BasicMetricChecker).MethodMetricRules)
        //More code here to include the other children, not sure if this can even work.
    .FirstOrDefault();

我不确定我是否遇到了 EF Core 的限制,或者我是否不知道可以解决此问题的某种机制。

如果是前者,我正在考虑将我的 Challenge 聚合序列化为 JSON object(我使用的是 postgresql)并删除这部分关系模型。虽然从领域的角度来看它是有意义的(我要么只需要挑战 header 或所有挑战 - 包括所有策略等),但我目前的研究表明 System.Text.Json 受到同样的限制并且我需要编写一个自定义 JSON转换器来启用此数据结构的序列化。

到目前为止我发现的最干净的选择是使用 Newtonsoft along with the JsonSubtypes 库,但我想检查我是否遗漏了一些可以帮助解决我的问题而不引入这些依赖项的东西(特别是因为 JsonSubtypes似乎不太活跃)。

这是我在遇到同样问题时利用反射创建的静态辅助方法。

把它想象成一个加强版 .Find()。它循环遍历 class 的所有属性并以与您在代码中使用 .Include().ThenInclude() 模式时相同的方式加载它们。 这在第一个 if/else 块中确实有一个额外的内容,它从为 Type 参数传入的接口中找到 DbContext 模型。在我们的案例中,我们使用了相当多的接口来 class 化我们的模型 classes(认为它可能有用)。这有一些巨大的开销,所以要小心。我们将 FullFindAsync 用于非常具体的用例,并且通常尝试做 .Includes().

如果您想更深入地了解加载的内容,您还可以在末尾的 foreach 循环中添加进一步的深度搜索。同样,这也有一些巨大的开销,因此请确保在大型数据集上对其进行测试。

public static async Task<object> FullFindAsync(this DbContext context, Type entityType, params object[] keyValues)
{
   object entity = null;

   //handle an Interface as a passed in type
   if (entityType.IsInterface)
   {
      //get all Types of Models in the DbContext that implement the Interface
      var implementedModels = context.Model.GetEntityTypes()
                               .Select(et => et.ClrType)
                               .Where(t => entityType.IsAssignableFrom(t))
                               .ToList();

      //loop through all Models and try to find the object via the parameters
      foreach (var modelType in implementedModels)
      {
         entity = await context.FindAsync(modelType, keyValues);
         if (entity != null) break;
      }
   }
   else
    //find the object, via regular Find
    entity = await context.FindAsync(entityType, keyValues);

   //didnt find the object
   if (entity == null) return null;

   //loop through all navigation properties
   foreach (var navigation in entity.GetType().GetProperties())
   {
      //skip NotMapped properties (to avoid context collection error)
      if (context.Entry(entity).Metadata.FindNavigation(navigation.Name) == null)
                    continue;

      //check and load reference and collection properties
      if (typeof(IDatabaseObject).IsAssignableFrom(navigation.PropertyType))
                    context.Entry(entity).Reference(navigation.Name).Load();
      if (typeof(IEnumerable<object>).IsAssignableFrom(navigation.PropertyType))
                    context.Entry(entity).Collection(navigation.Name).Load();
   }

   return entity;
}


//***USAGES***
var fullModel = await _dbContext.FullFindAsync(typeof(MyModelObject), model.Id);
var correctModel = await _dbContext.FullFindAsync(typeof(IModelInterface), someRandomId);