НейроАгент

Как избежать дублирования кода в методах Include Entity Framework

Изучите проверенные методы для устранения дублирования кода при использовании метода .Include Entity Framework в нескольких классах. Узнайте о расширениях методах, шаблонах репозиториев и лучших практиках для создания чистых и поддерживаемых запросов.

Вопрос

Как избежать дублирования кода при использовании метода .Include Entity Framework в нескольких классах?

Я работаю с Entity Framework и у меня есть несколько классов, которым требуются похожие цепочки .Include и .ThenInclude. Например:

csharp
query = (from n in currentDBContext.FBBuchungenCollection
              .Include(x => x.BelegHerkunft)
              .Include(x => x.Buchungsordner)
              .Include(x => x.Buchungsperiode).ThenInclude(x => x.Geschaeftsjahr)
              .Include(x => x.BuchungsUser)
              .Include(x => x.Erfassungsart)
              .Include(x => x.ErstellUser)
              .Include(x => x.Mandant).ThenInclude(x => x.Haupt Adresse)
              .Include(x => x.StornoUser)
              .Include(x => x.Teilbuchungen).ThenInclude(x => x.FremdWaehrung)
              .Include(x => x.Teilbuchungen).ThenInclude(x => x.KKArt)
              .Include(x => x.Teilbuchungen).ThenInclude(x => x.KKKonto)                         
              .Include(x => x.Teilbuchungen).ThenInclude(x => x.Konto)
              .Include(x => x.Teilbuchungen).ThenInclude(x => x.KoReVerteilung).ThenInclude(x => x.Periodenverteilungen).ThenInclude(x => x.Kontierungen).ThenInclude(x => x.Kontierungsangaben).ThenInclude(x => x.KontierungsangabenKTR).ThenInclude(x => x.Kostentraeger)
              .Include(x => x.Teilbuchungen).ThenInclude(x => x.KoReVerteilung).ThenInclude(x => x.Periodenverteilungen).ThenInclude(x => x.Kontierungen).ThenInclude(x => x.Kontierungsangaben).ThenInclude(x => x.KontierungsangabenKTR).ThenInclude(x => x.KostentraegerArt)                         
              .Include(x => x.Teilbuchungen).ThenInclude(x => x.OffenerPosten).ThenInclude(x => x.Kostenstelle)
              .Include(x => x.Teilbuchungen).ThenInclude(x => x.OffenerPosten).ThenInclude(x => x.Kostentraeger)
              .Include(x => x.Teilbuchungen).ThenInclude(x => x.OffenerPosten).ThenInclude(x => x.Mandant)
              .Include(x => x.Teilbuchungen).ThenInclude(x => x.OffenerPosten).ThenInclude(x => x.Sachkonto)
              .Include(x => x.Teilbuchungen).ThenInclude(x => x.Steuerschluessel)
              .Include(x => x.Teilbuchungen).ThenInclude(x => x.VerweisTeilbuchung)
          select n);

И еще один пример:

csharp
query = (from n in currentDBContext.KKKontoCollection
             .TagWith("KKKonto.BuildQuery")
             .Include(x => x.Mandant).ThenInclude(x => x.Haupt Adresse)
             .Include(x => x.KKArt)
             .Include(x => x.Land)
             .Include(x => x.Mahnkategorie)
             .Include(x => x.SammelKonto)
             .Include(x => x.Zahlungsbedingung)
         select n);

Обратите внимание, что в обоих примерах содержится одна и та же строка:

csharp
.Include(x => x.Mandant).ThenInclude(x => x.Haupt Adresse)

Существует ли способ создать повторно используемые методы для общих шаблонов включения? Например, метод вроде AddIncludeMandant(), который можно использовать в разных классах для избежания такого дублирования?

Каковы лучшие практики организации запросов Entity Framework для минимизации дублирования кода при сохранении читаемости и производительности?

НейроАгент

Вы можете избежать дублирования кода при использовании метода .Include в Entity Framework, создаваемые повторно используемые методы расширения и эффективно организуя логику запросов. Наиболее распространенный подход включает создание методов расширения, которые инкапсулируют общие шаблоны включения, позволяя повторно использовать одни и те же цепи навигации в нескольких запросах без дублирования кода.


Содержание


Методы расширения для общих шаблонов включения

Наиболее прямое решение вашей проблемы - создание методов расширения, которые инкапсулируют ваши общие цепи включения. Этот подход позволяет повторно использовать одни и те же шаблоны навигации в разных запросах DbSet.

Базовый подход с методами расширения

csharp
public static class EntityFrameworkExtensions
{
    public static IQueryable<FBBuchungen> IncludeStandardBuchungIncludes(this IQueryable<FBBuchungen> query)
    {
        return query
            .Include(x => x.BelegHerkunft)
            .Include(x => x.Buchungsordner)
            .Include(x => x.Buchungsperiode).ThenInclude(x => x.Geschaeftsjahr)
            .Include(x => x.BuchungsUser)
            .Include(x => x.Erfassungsart)
            .Include(x => x.ErstellUser)
            .Include(x => x.Mandant).ThenInclude(x => x.Haupt_Adresse)
            .Include(x => x.StornoUser)
            .Include(x => x.Teilbuchungen).ThenInclude(x => x.FremdWaehrung)
            .Include(x => x.Teilbuchungen).ThenInclude(x => x.KKArt)
            .Include(x => x.Teilbuchungen).ThenInclude(x => x.KKKonto)
            .Include(x => x.Teilbuchungen).ThenInclude(x => x.Konto)
            .Include(x => x.Teilbuchungen).ThenInclude(x => x.KoReVerteilung).ThenInclude(x => x.Periodenverteilungen)
            .Include(x => x.Teilbuchungen).ThenInclude(x => x.OffenerPosten).ThenInclude(x => x.Kostenstelle)
            .Include(x => x.Teilbuchungen).ThenInclude(x => x.OffenerPosten).ThenInclude(x => x.Kostentraeger)
            .Include(x => x.Teilbuchungen).ThenInclude(x => x.OffenerPosten).ThenInclude(x => x.Mandant)
            .Include(x => x.Teilbuchungen).ThenInclude(x => x.OffenerPosten).ThenInclude(x => x.Sachkonto)
            .Include(x => x.Teilbuchungen).ThenInclude(x => x.Steuerschluessel)
            .Include(x => x.Teilbuchungen).ThenInclude(x => x.VerweisTeilbuchung);
    }

    public static IQueryable<KKKonto> IncludeStandardKontoIncludes(this IQueryable<KKKonto> query)
    {
        return query
            .Include(x => x.Mandant).ThenInclude(x => x.Haupt_Adresse)
            .Include(x => x.KKArt)
            .Include(x => x.Land)
            .Include(x => x.Mahnkategorie)
            .Include(x => x.SammelKonto)
            .Include(x => x.Zahlungsbedingung);
    }
}

Пример использования

csharp
// Вместо длинной цепочки используйте ваш метод расширения
var query = currentDBContext.FBBuchungenCollection
    .IncludeStandardBuchungIncludes()
    .Where(x => x.SomeCondition);

// Для KKKonto
var kontoQuery = currentDBContext.KKKontoCollection
    .IncludeStandardKontoIncludes()
    .Where(x => x.SomeCondition);

Модульный подход с композируемыми расширениями

Для лучшей организации вы можете разбить ваши включения на более мелкие, композируемые методы:

csharp
public static class EntityFrameworkExtensions
{
    // Общие включения, используемые в нескольких типах сущностей
    public static IQueryable<T> IncludeCommonNavigation<T>(this IQueryable<T> query) where T : class
    {
        var type = typeof(T);
        if (type == typeof(FBBuchungen))
        {
            return (query as IQueryable<FBBuchungen>)
                .IncludeStandardBuchungIncludes() as IQueryable<T>;
        }
        else if (type == typeof(KKKonto))
        {
            return (query as IQueryable<KKKonto>)
                .IncludeStandardKontoIncludes() as IQueryable<T>;
        }
        return query;
    }

    // Добавляем специфические включения для общих шаблонов навигации
    public static IQueryable<T> IncludeMandantWithDetails<T>(this IQueryable<T> query) where T : class
    {
        return query.Include("Mandant.Haupt_Adresse") as IQueryable<T>;
    }
}

Согласно обсуждению на Reddit о повторном использовании запросов .NET Core, “это обеспечивает большую гибкость, если методы расширения находятся в IQueryable, а не в DbSet, поскольку это позволяет составлять вместе несколько повторно используемых запросов.”


Шаблон репозитория с повторно используемыми включениями

Другой подход - реализация шаблона репозитория, который инкапсулирует общую логику включения:

csharp
public interface IRepository<T> where T : class
{
    IQueryable<T> GetAllIncluding(params Expression<Func<T, object>>[] includeProperties);
    IQueryable<T> GetAll();
}

public class GenericRepository<T> : IRepository<T> where T : class
{
    protected readonly DbContext _context;
    protected readonly DbSet<T> _dbSet;

    public GenericRepository(DbContext context)
    {
        _context = context;
        _dbSet = context.Set<T>();
    }

    public virtual IQueryable<T> GetAllIncluding(params Expression<Func<T, object>>[] includeProperties)
    {
        IQueryable<T> query = _dbSet;

        foreach (var includeProperty in includeProperties)
        {
            query = query.Include(includeProperty);
        }

        return query;
    }

    public virtual IQueryable<T> GetAll()
    {
        return _dbSet;
    }
}

// Специализированный репозиторий с предопределенными включениями
public class BuchungRepository : GenericRepository<FBBuchungen>
{
    public BuchungRepository(DbContext context) : base(context) { }

    public IQueryable<FBBuchungen> GetAllWithStandardIncludes()
    {
        return _dbSet
            .Include(x => x.BelegHerkunft)
            .Include(x => x.Mandant).ThenInclude(x => x.Haupt_Adresse)
            // ... другие включения
            .Include(x => x.Teilbuchungen).ThenInclude(x => x.KoReVerteilung);
    }
}

Как упоминается в обсуждении на StackOverflow о шаблонах репозитория, централизованная логика включения помогает избежать дублирования, сохраняя при этом чистое разделение ответственности.


Вспомогательные классы DbContext

Вы можете создавать вспомогательные классы, которые динамически генерируют выражения включения:

csharp
public static class DbContextHelper
{
    public static Func<IQueryable<T>, IQueryable<T>> GetNavigations<T>() where T : class
    {
        var type = typeof(T);
        
        if (type == typeof(FBBuchungen))
        {
            return query => query
                .Include(x => x.Mandant).ThenInclude(x => x.Haupt_Adresse)
                .Include(x => x.Teilbuchungen).ThenInclude(x => x.OffenerPosten)
                // ... другие включения
                ;
        }
        else if (type == typeof(KKKonto))
        {
            return query => query
                .Include(x => x.Mandant).ThenInclude(x => x.Haupt_Adresse)
                .Include(x => x.KKArt)
                // ... другие включения
                ;
        }
        
        return query => query;
    }
}

// Использование
var includes = DbContextHelper.GetNavigations<FBBuchungen>();
var query = includes(currentDBContext.FBBuchungenCollection);

Сторонние решения

Несколько сторонних библиотек могут помочь с повторным использованием запросов:

NeinLinq.EntityFrameworkCore

Как упоминается в ответе на StackOverflow о методах расширения, NeinLinq “расширяет поставщиков LINQ, таких как Entity Framework, чтобы включать повторное использование функций, перезапросы и построение динамических запросов с помощью переводимых предикатов и селекторов.”

Expressionify

Репозиторий Expressionify на GitHub предоставляет способ использования методов расширения в запросах Entity Framework Core, хотя, как указано в документации, “это заставляет Entity Framework Core выполнять запрос в памяти, а не в базе данных.”

Entity Framework Extensions

В основном сосредоточившись на производительности, Entity Framework Extensions также предоставляет утилиты для управления запросами.


Лучшие практики организации

1. Иерархическая организация

Организуйте ваши включения иерархически:

csharp
public static class EntityFrameworkExtensions
{
    // Уровень 1: Базовые включения
    public static IQueryable<T> IncludeBasicNavigation<T>(this IQueryable<T> query) where T : class
    {
        return query
            .Include(x => x.Mandant)
            .Include(x => x.ErstellUser);
    }

    // Уровень 2: Детальные включения
    public static IQueryable<T> IncludeDetailedNavigation<T>(this IQueryable<T> query) where T : class
    {
        return query
            .IncludeBasicNavigation()
            .Include(x => x.Mandant).ThenInclude(x => x.Haupt_Adresse)
            .Include(x => x.Teilbuchungen);
    }

    // Уровень 3: Сложные включения
    public static IQueryable<T> IncludeComplexNavigation<T>(this IQueryable<T> query) where T : class
    {
        return query
            .IncludeDetailedNavigation()
            .Include(x => x.Teilbuchungen).ThenInclude(x => x.KoReVerteilung).ThenInclude(x => x.Periodenverteilungen);
    }
}

2. Шаблон Fluent Interface

Создайте fluent interface для построения сложных запросов:

csharp
public static class QueryBuilder
{
    public static EntityQuery<T> StartWith<T>(IQueryable<T> query) where T : class
    {
        return new EntityQuery<T>(query);
    }
}

public class EntityQuery<T> where T : class
{
    private readonly IQueryable<T> _query;

    public EntityQuery(IQueryable<T> query)
    {
        _query = query;
    }

    public EntityQuery<T> IncludeMandant()
    {
        _query = _query.Include(x => x.Mandant).ThenInclude(x => x.Haupt_Adresse);
        return this;
    }

    public EntityQuery<T> IncludeTeilbuchungenWithOffenePosten()
    {
        _query = _query
            .Include(x => x.Teilbuchungen)
            .Include(x => x.Teilbuchungen).ThenInclude(x => x.OffenerPosten);
        return this;
    }

    public IQueryable<T> Build()
    {
        return _query;
    }
}

// Использование
var query = QueryBuilder.StartWith(currentDBContext.FBBuchungenCollection)
    .IncludeMandant()
    .IncludeTeilbuchungenWithOffenePosten()
    .Build();

3. Маркированные запросы для отладки

Как показано в вашем примере, используйте TagWith для идентификации запросов:

csharp
public static class EntityFrameworkExtensions
{
    public static IQueryable<T> TagWithStandardIncludes<T>(this IQueryable<T> query) where T : class
    {
        return query.TagWith("StandardIncludes_" + typeof(T).Name);
    }
}

// Использование
var query = currentDBContext.FBBuchungenCollection
    .TagWithStandardIncludes()
    .IncludeStandardBuchungIncludes();

Рассмотрения производительности

1. Избегайте избыточных включений

Будьте осторожны, не включайте больше данных, чем необходимо. Как указано в документации Microsoft, “указывает связанные сущности для включения в результаты запроса. Свойство навигации, которое нужно включить, указывается начиная с типа сущности, запрашиваемой (TEntity).”

2. Используйте селективные включения

Создайте несколько методов включения для разных сценариев:

csharp
public static class EntityFrameworkExtensions
{
    public static IQueryable<FBBuchungen> IncludeSummaryOnly(this IQueryable<FBBuchungen> query)
    {
        return query
            .Include(x => x.Mandant)
            .Include(x => x.Buchungsperiode);
    }

    public static IQueryable<FBBuchungen> IncludeFullDetails(this IQueryable<FBBuchungen> query)
    {
        return query.IncludeStandardBuchungIncludes();
    }
}

3. Мониторинг производительности запросов

Используйте логирование для мониторинга ваших запросов:

csharp
// В вашем DbContext
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information)
        .EnableSensitiveDataLogging();
}

Источники

  1. r/dotnet на Reddit: Как повторно использовать запросы в .NET Core с Entity Framework без “громоздкого” репозитория?
  2. StackOverflow: Как написать метод репозитория для .ThenInclude в EF Core 2
  3. StackOverflow: Entity Framework Core - Использование методов расширения внутри Queryable
  4. Microsoft Learn: Метод QueryableExtensions.Include (EF6)
  5. Microsoft Learn: Метод EntityFrameworkQueryableExtensions.Include (EF Core)
  6. GitHub - ClaveConsulting/Expressionify: Использование методов расширения в запросах Entity Framework Core
  7. Обзор Entity Framework Extensions

Заключение

Чтобы избежать дублирования кода при использовании метода .Include в Entity Framework, рассмотрите эти ключевые стратегии:

  1. Создавайте методы расширения для общих шаблонов включения, позволяя повторно использовать одни и те же цепи навигации в нескольких запросах
  2. Используйте шаблон репозитория для централизации логики включения и поддержания разделения ответственности
  3. Реализуйте иерархическую организацию включений с базовыми, детальными и сложными уровнями
  4. Рассмотрите сторонние библиотеки, такие как NeinLinq, для расширенных возможностей построения запросов
  5. Используйте маркированные запросы для лучшей отладки и мониторинга
  6. Будьте избирательны в отношении включений, чтобы избежать проблем с производительностью

Подход с методами расширения обычно является наиболее прямым и сохраняет читаемость, устраняя дублирование. Начните с простых повторно используемых методов включения и постепенно создавайте более сложные композируемые шаблоны по мере роста вашего приложения. Помните о мониторинге производительности запросов и корректировке вашей стратегии включения на основе фактических шаблонов использования.