Добавить включение на уровне DbContext - PullRequest
0 голосов
/ 11 июля 2020

Я хочу реализовать что-то похожее на ленивую загрузку, но не понимаю, как это реализовать. Я хочу заставить ядро ​​структуры сущностей включать свойство навигации для всех запросов для типа, который реализует мой интерфейс

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 каким-то образом включить это свойство, это тоже нормально для меня.

Как я могу этого добиться?

1 Ответ

1 голос
/ 12 июля 2020
• 1000 вы можете подключиться к самому началу конвейера запросов и ввести вызов 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");
        }
    }
}

Несмотря на то, что в запросе в Main() не делается явного Include(), создается следующий 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]
...