НейроАгент

Устранение дублирования кода с помощью методов Include в EF Core

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

Вопрос

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

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

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(), которые я мог бы использовать в любом классе, которому нужны эти включения.

НейроАгент

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

Создание повторно используемых методов расширения

Рекомендуемый подход — создание методов расширения для IQueryable<T>, которые можно комбинировать вместе. Это обеспечивает максимальную гибкость и возможность повторного использования во всем приложении.

csharp
public static class EntityFrameworkExtensions
{
    // Общий метод для добавления стандартных включений для Mandant
    public static IQueryable<T> IncludeMandant<T>(this IQueryable<T> query) where T : class
    {
        // Это потребовало бы проверки типа во время выполнения или базового интерфейса
        // Вместо этого создадим более конкретные методы
        return query;
    }

    // Конкретные методы для общих шаблонов
    public static IQueryable<FBBuchungen> IncludeFullBuchungData(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)
                .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);
    }

    // Общие включения, которые можно повторно использовать для разных сущностей
    public static IQueryable<Mandant> IncludeMandantDetails(this IQueryable<Mandant> query)
    {
        return query.Include(x => x.Haupt_Adresse);
    }

    public static IQueryable<KKKonto> IncludeKKKontoDetails(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
// До
query = (from n in currentDBContext.FBBuchungenCollection
              .Include(x => x.BelegHerkunft)
              .Include(x => x.Buchungsordner)
              .Include(x => x.Buchungsperiode).ThenInclude(x => x.Geschaeftsjahr)
              // ... еще много включений
          select n);

// После
query = (from n in currentDBContext.FBBuchungenCollection
              .IncludeFullBuchungData()
          select n);

// Для KKKonto
query = (from n in currentDBContext.KKKontoCollection
             .TagWith("KKKonto.BuildQuery")
             .IncludeKKKontoDetails()
         select n);

Методы расширения для конкретных сущностей

Создавайте методы расширения для каждого типа сущностей, которые инкапсулируют их специфические требования к включениям. Этот подход показан в примере на Stack Overflow, где они создали PopulateWithTeacherAndLicense для IQueryable<Subject>.

csharp
public static class EntityFrameworkExtensions
{
    // Методы для FBBuchungen
    public static IQueryable<FBBuchungen> IncludeStandardBuchungData(this IQueryable<FBBuchungen> query)
    {
        return query
            .Include(x => x.BelegHerkunft)
            .Include(x => x.Buchungsordner)
            .Include(x => x.Buchungsperiode)
            .Include(x => x.BuchungsUser)
            .Include(x => x.Erfassungsart)
            .Include(x => x.ErstellUser)
            .Include(x => x.StornoUser);
    }

    public static IQueryable<FBBuchungen> IncludeTeilbuchungenWithDetails(this IQueryable<FBBuchungen> query)
    {
        return query
            .Include(x => x.Teilbuchungen)
            .ThenInclude(x => x.FremdWaehrung)
            .ThenInclude(x => x.KKArt)
            .ThenInclude(x => x.KKKonto)
            .ThenInclude(x => x.Konto)
            .ThenInclude(x => x.KoReVerteilung)
                .ThenInclude(x => x.Periodenverteilungen)
                .ThenInclude(x => x.Kontierungen)
                .ThenInclude(x => x.Kontierungsangaben)
                .ThenInclude(x => x.KontierungsangabenKTR);
    }

    // Методы для KKKonto
    public static IQueryable<KKKonto> IncludeStandardKKKontoData(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);
    }

    // Общие включения, которые можно разделить
    public static IQueryable<T> IncludeMandantAddress<T>(this IQueryable<T> query) where T : class, IMandantEntity
    {
        // Это предполагает интерфейс IMandantEntity со свойством Mandant
        return query.Include(x => x.Mandant).ThenInclude(x => x.Haupt_Adresse);
    }
}

Для использования общего метода вам нужно создать интерфейс:

csharp
public interface IMandantEntity
{
    Mandant Mandant { get; set; }
}

// Ваши сущности будут реализовывать этот интерфейс
public class FBBuchungen : IMandantEntity
{
    public Mandant Mandant { get; set; }
    // другие свойства...
}

public class KKKonto : IMandantEntity
{
    public Mandant Mandant { get; set; }
    // другие свойства...
}

Решения с использованием деревьев выражений

Для более сложных сценариев, где требуются динамические включения, рассмотрите использование деревьев выражений. Как упоминалось в ответе на Stack Overflow, деревья выражений предоставляют способ создания повторно используемых селекторов, которые EF может преобразовать в SQL.

csharp
public static class EntityFrameworkExtensions
{
    // Общий метод для добавления включений на основе селекторов свойств
    public static IQueryable<T> Includes<T>(
        this IQueryable<T> query,
        params Expression<Func<T, object>>[] includes) where T : class
    {
        foreach (var include in includes)
        {
            query = query.Include(include);
        }
        return query;
    }

    // Более сложный пример с вложенными включениями
    public static IQueryable<FBBuchungen> IncludeBuchungenWithExpression(
        this IQueryable<FBBuchungen> query,
        Expression<Func<FBBuchungen, object>> includeSelector)
    {
        // Здесь потребовалось бы более сложное манипулирование деревьями выражений
        // Пока что покажем более простой подход
        return query;
    }
}

Использование:

csharp
query = currentDBContext.FBBuchungenCollection
    .Includes(
        x => x.BelegHerkunft,
        x => x.Buchungsordner,
        x => x.Mandant.Haupt_Adresse,
        x => x.Teilbuchungen
    );

Интеграция с шаблоном Repository

Вы можете интегрировать эти методы расширения в шаблон Repository для лучшей организации. Как упоминалось в ответе на Stack Overflow, вы можете создать вспомогательные методы, которые управляют включениями.

csharp
public class GenericRepository<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 async Task<IEnumerable<T>> GetAllAsync(
        Expression<Func<T, bool>> filter = null,
        Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null,
        string includeProperties = null)
    {
        IQueryable<T> query = _dbSet;

        if (filter != null)
        {
            query = query.Where(filter);
        }

        if (includeProperties != null)
        {
            foreach (var includeProperty in includeProperties.Split(
                new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
            {
                query = query.Include(includeProperty);
            }
        }

        if (orderBy != null)
        {
            return await orderBy(query).ToListAsync();
        }
        else
        {
            return await query.ToListAsync();
        }
    }
}

// Специализированные репозитории могут использовать методы расширения
public class BuchungenRepository : GenericRepository<FBBuchungen>
{
    public BuchungenRepository(DbContext context) : base(context) { }

    public async Task<IEnumerable<FBBuchungen>> GetFullBuchungenAsync()
    {
        return await _dbSet.IncludeFullBuchungData().ToListAsync();
    }
}

Лучшие практики и рекомендации

Вопросы производительности

Будьте внимательны к последствиям для производительности при обширных включениях, как упоминалось в блоге JetBrains. Рассмотрите использование AsSplitQuery() при работе с отношениями “многие-ко-многим” для избежания дублирования данных.

csharp
query = currentDBContext.FBBuchungenCollection
    .AsSplitQuery() // Уменьшает дублирование данных
    .IncludeFullBuchungData();

Компонуемые запросы

Как отмечено в обсуждении на Reddit, сохранение методов расширения для IQueryable вместо DbSet позволяет лучшую компоновку:

csharp
// Хорошо: Метод расширения для IQueryable
public static IQueryable<T> IncludeBasicData<T>(this IQueryable<T> query) where T : class
{
    return query.Include(x => x.BasicProperty);
}

// Это позволяет компоновку
var result = context.Entities
    .Where(x => x.IsActive)
    .IncludeBasicData()
    .IncludeAdvancedData();

Библиотеки сторонних разработчиков

Для более сложных сценариев рассмотрите библиотеки, такие как NeinLinq.EntityFrameworkCore, упомянутые в результатах поиска, которые позволяют повторно использовать функции в запросах EF.

csharp
// Использование NeinLinq для вызова методов в запросах
var users = await db.Users
    .Where(user => user.IsOver18()) // Это обычно не сработает
    .ToListAsync();

Тестирование и поддерживаемость

При создании этих методов расширения убедитесь, что они:

  • Тестируемые: Пишите модульные тесты для ваших методов расширения
  • Документированные: Используйте XML-документацию для объяснения, что включает каждый метод
  • Версионируемые: Рассмотрите использование семантического версионирования, если они используются в нескольких проектах
csharp
/// <summary>
/// Включает все стандартные данные Buchungen с полными навигационными свойствами
/// </summary>
/// <param name="query">Исходный запрос</param>
/// <returns>Запрос с примененными включениями</returns>
public static IQueryable<FBBuchungen> IncludeFullBuchungData(this IQueryable<FBBuchungen> query)
{
    // Реализация...
}

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

Источники

  1. Entity Framework Core - Call A Populate Extension Method Inside Include - Stack Overflow
  2. Entity Framework Extensions Overview
  3. Entity Framework Core 5 - Pitfalls To Avoid and Ideas to Try | The .NET Tools Blog
  4. How do you reuse queries in .NET Core with Entity Framework without a “clunky” repository? - Reddit
  5. Entity Framework Core - Use Extension Methods Inside Queryable - Stack Overflow
  6. EntityFrameworkQueryableExtensions.Include Method - Microsoft Learn
  7. How to write Repository method for .ThenInclude in EF Core 2 - Stack Overflow

Заключение

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

  1. Создавайте методы расширения для IQueryable, чтобы инкапсулировать общие шаблоны включения, позволяя гибкую компоновку в разных запросах.

  2. Стройте как методы расширения для конкретных сущностей, так и общие методы — методы для конкретных сущностей для сложных цепочек включений и общие методы для общих шаблонов, таких как IncludeMandantAddress().

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

  4. Интегрируйте с шаблоном Repository, включив эти методы расширения в специализированные репозитории для лучшей организации.

  5. Следуйте лучшим практикам производительности, используя AsSplitQuery() для сложных отношений и будучи внимательным к избыточной выборке данных.

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