首页 > 解决方案 > 在 DbContext 级别添加包含

问题描述

我想实现类似于延迟加载的东西,但不明白如何实现。我想强制实体框架核心包含实现我的接口的类型的所有查询的导航属性

public interface IMustHaveOrganisation
{
    Guid OrganisationId { get; set; }
    Organisation Organisation { get; set; }
}

public class MyEntity : IMustHaveOrganisation {
    public Guid OrganisationId { get; set; }
    public virtual Organisation Organisation { get; set; }
}

如果没有延迟加载,我需要将 .Include(x=>x.Organisation) 添加到每个查询字面上,并且我不能使用 Microsoft 提供的延迟加载实现。我需要一种自定义实现,只加载一个属性。甚至以某种方式强制 DbContext 包含该属性,这对我来说也很好。

我怎样才能做到这一点?

标签: entity-framework-core

解决方案


在 EF Core 翻译它之前,您可以通过重写表达式树来完成这项工作。

为了使这项工作以某种方式工作,您不必在查询中指定任何其他内容,您可以挂钩到查询管道的最开始并Include()根据需要注入调用。

这可以通过指定自定义IQueryTranslationPreprocessorFactory实现来完成。

以下完全工作的控制台项目演示了这种方法:

using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace IssueConsoleTemplate
{
    public class Organisation
    {
        public int OrganisationId { get; set; }
        public string Name { get; set; }
    }

    public interface IMustHaveOrganisation
    {
        int OrganisationId { get; set; }
        Organisation Organisation { get; set; }
    }

    public class MyEntity : IMustHaveOrganisation
    {
        public int MyEntityId { get; set; }
        public string Name { get; set; }
        
        public int OrganisationId { get; set; }
        public virtual Organisation Organisation { get; set; }
    }

    public class CustomQueryTranslationPreprocessorFactory : IQueryTranslationPreprocessorFactory
    {
        private readonly QueryTranslationPreprocessorDependencies _dependencies;
        private readonly RelationalQueryTranslationPreprocessorDependencies _relationalDependencies;

        public CustomQueryTranslationPreprocessorFactory(
            QueryTranslationPreprocessorDependencies dependencies,
            RelationalQueryTranslationPreprocessorDependencies relationalDependencies)
        {
            _dependencies = dependencies;
            _relationalDependencies = relationalDependencies;
        }

        public virtual QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext)
            => new CustomQueryTranslationPreprocessor(_dependencies, _relationalDependencies, queryCompilationContext);
    }

    public class CustomQueryTranslationPreprocessor : RelationalQueryTranslationPreprocessor
    {
        public CustomQueryTranslationPreprocessor(
            QueryTranslationPreprocessorDependencies dependencies,
            RelationalQueryTranslationPreprocessorDependencies relationalDependencies,
            QueryCompilationContext queryCompilationContext)
            : base(dependencies, relationalDependencies, queryCompilationContext)
        {
        }

        public override Expression Process(Expression query)
        {
            query = new DependenciesIncludingExpressionVisitor().Visit(query);
            return base.Process(query);
        }
    }
    
    public class DependenciesIncludingExpressionVisitor : ExpressionVisitor
    {
        protected override Expression VisitConstant(ConstantExpression node)
        {
            // Call Include("Organisation"), if SomeEntity in a
            // DbSet<SomeEntity> implements IMustHaveOrganisation.
            if (node.Type.IsGenericType &&
                node.Type.GetGenericTypeDefinition() == typeof(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable<>) &&
                node.Type.GenericTypeArguments.Length == 1 &&
                typeof(IMustHaveOrganisation).IsAssignableFrom(node.Type.GenericTypeArguments[0]))
            {
                return Expression.Call(
                    typeof(EntityFrameworkQueryableExtensions),
                    nameof(EntityFrameworkQueryableExtensions.Include),
                    new[] {node.Type.GenericTypeArguments[0]},
                    base.VisitConstant(node),
                    Expression.Constant(nameof(IMustHaveOrganisation.Organisation)));
            }

            return base.VisitConstant(node);
        }
    }

    public class Context : DbContext
    {
        public DbSet<MyEntity> MyEntities { get; set; }
        public DbSet<Organisation> Organisations { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            // Register the custom IQueryTranslationPreprocessorFactory implementation.
            // Since this is a console program, we need to create our own
            // ServiceCollection for this.
            // In an ASP.NET Core application, the AddSingleton call can just be added to
            // the general service configuration method.
            var serviceProvider = new ServiceCollection()
                .AddEntityFrameworkSqlServer()
                .AddSingleton<IQueryTranslationPreprocessorFactory, CustomQueryTranslationPreprocessorFactory>()
                .AddScoped(
                    s => LoggerFactory.Create(
                        b => b
                            .AddConsole()
                            .AddFilter(level => level >= LogLevel.Information)))
                .BuildServiceProvider();

            optionsBuilder
                .UseInternalServiceProvider(serviceProvider) // <-- use our ServiceProvider
                .UseSqlServer(@"Data Source=.\MSSQL14;Integrated Security=SSPI;Initial Catalog=62849896")
                .EnableSensitiveDataLogging()
                .EnableDetailedErrors();
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<MyEntity>(
                entity =>
                {
                    entity.HasData(
                        new MyEntity {MyEntityId = 1, Name = "First Entity", OrganisationId = 1 },
                        new MyEntity {MyEntityId = 2, Name = "Second Entity", OrganisationId = 1 },
                        new MyEntity {MyEntityId = 3, Name = "Third Entity", OrganisationId = 2 });
                });

            modelBuilder.Entity<Organisation>(
                entity =>
                {
                    entity.HasData(
                        new Organisation {OrganisationId = 1, Name = "First Organisation"},
                        new Organisation {OrganisationId = 2, Name = "Second Organisation"});
                });
        }
    }

    internal static class Program
    {
        private static void Main()
        {
            using var context = new Context();

            context.Database.EnsureDeleted();
            context.Database.EnsureCreated();

            var myEntitiesWithOrganisations = context.MyEntities
                .OrderBy(i => i.MyEntityId)
                .ToList();
            
            Debug.Assert(myEntitiesWithOrganisations.Count == 3);
            Debug.Assert(myEntitiesWithOrganisations[0].Name == "First Entity");
            Debug.Assert(myEntitiesWithOrganisations[0].Organisation.Name == "First Organisation");
        }
    }
}

即使Include()在 中的查询中没有明确说明Main(),也会生成以下 SQL,它连接和检索Organisation实体:

SELECT [m].[MyEntityId], [m].[Name], [m].[OrganisationId], [o].[OrganisationId], [o].[Name]
FROM [MyEntities] AS [m]
INNER JOIN [Organisations] AS [o] ON [m].[OrganisationId] = [o].[OrganisationId]
ORDER BY [m].[MyEntityId]

推荐阅读