Как избежать дублирования кода при использовании метода .Include Entity Framework в нескольких классах?
Я работаю с Entity Framework и у меня есть несколько классов, которые используют метод .Include для загрузки связанных сущностей. Каждый класс имеет схожий шаблон включений, что приводит к дублированию кода. Например:
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);
Другой пример из другого класса:
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);
Как видно, некоторые включения повторяются в разных классах, например:
.Include(x => x.Mandant).ThenInclude(x => x.Haupt_Adresse)
Есть ли способ избежать этого дублирования? Я ищу решение, которое позволило бы мне создавать повторно используемые методы, такие как AddIncludeMandant(), которые я мог бы использовать в любом классе, которому нужны эти включения.
Метод Include в Entity Framework часто приводит к повторяющемуся коду, когда для нескольких запросов необходимо загружать связанные сущности. Наиболее эффективным решением является создание повторно используемых методов расширения, которые инкапсулируют общие шаблоны включения. Вот несколько подходов для устранения этого дублирования:
- Создание повторно используемых методов расширения
- Методы расширения для конкретных сущностей
- Решения с использованием деревьев выражений
- Интеграция с шаблоном Repository
- Лучшие практики и рекомендации
Создание повторно используемых методов расширения
Рекомендуемый подход — создание методов расширения для IQueryable<T>, которые можно комбинировать вместе. Это обеспечивает максимальную гибкость и возможность повторного использования во всем приложении.
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);
}
}
Теперь вы можете значительно упростить свои запросы:
// До
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>.
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);
}
}
Для использования общего метода вам нужно создать интерфейс:
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.
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;
}
}
Использование:
query = currentDBContext.FBBuchungenCollection
.Includes(
x => x.BelegHerkunft,
x => x.Buchungsordner,
x => x.Mandant.Haupt_Adresse,
x => x.Teilbuchungen
);
Интеграция с шаблоном Repository
Вы можете интегрировать эти методы расширения в шаблон Repository для лучшей организации. Как упоминалось в ответе на Stack Overflow, вы можете создать вспомогательные методы, которые управляют включениями.
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() при работе с отношениями “многие-ко-многим” для избежания дублирования данных.
query = currentDBContext.FBBuchungenCollection
.AsSplitQuery() // Уменьшает дублирование данных
.IncludeFullBuchungData();
Компонуемые запросы
Как отмечено в обсуждении на Reddit, сохранение методов расширения для IQueryable вместо DbSet позволяет лучшую компоновку:
// Хорошо: Метод расширения для 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.
// Использование NeinLinq для вызова методов в запросах
var users = await db.Users
.Where(user => user.IsOver18()) // Это обычно не сработает
.ToListAsync();
Тестирование и поддерживаемость
При создании этих методов расширения убедитесь, что они:
- Тестируемые: Пишите модульные тесты для ваших методов расширения
- Документированные: Используйте XML-документацию для объяснения, что включает каждый метод
- Версионируемые: Рассмотрите использование семантического версионирования, если они используются в нескольких проектах
/// <summary>
/// Включает все стандартные данные Buchungen с полными навигационными свойствами
/// </summary>
/// <param name="query">Исходный запрос</param>
/// <returns>Запрос с примененными включениями</returns>
public static IQueryable<FBBuchungen> IncludeFullBuchungData(this IQueryable<FBBuchungen> query)
{
// Реализация...
}
Реализуя эти шаблоны, вы можете значительно сократить дублирование кода, сохраняя при этом чистые, поддерживаемые и производительные запросы Entity Framework. Ключевым является выявление общих шаблонов включения и их инкапсуляция в повторно используемые методы расширения, которые можно комбинировать по мере необходимости.
Источники
- Entity Framework Core - Call A Populate Extension Method Inside Include - Stack Overflow
- Entity Framework Extensions Overview
- Entity Framework Core 5 - Pitfalls To Avoid and Ideas to Try | The .NET Tools Blog
- How do you reuse queries in .NET Core with Entity Framework without a “clunky” repository? - Reddit
- Entity Framework Core - Use Extension Methods Inside Queryable - Stack Overflow
- EntityFrameworkQueryableExtensions.Include Method - Microsoft Learn
- How to write Repository method for .ThenInclude in EF Core 2 - Stack Overflow
Заключение
Чтобы избежать дублирования кода при использовании метода Include в Entity Framework, реализуйте эти ключевые стратегии:
-
Создавайте методы расширения для IQueryable
, чтобы инкапсулировать общие шаблоны включения, позволяя гибкую компоновку в разных запросах. -
Стройте как методы расширения для конкретных сущностей, так и общие методы — методы для конкретных сущностей для сложных цепочек включений и общие методы для общих шаблонов, таких как IncludeMandantAddress().
-
Рассмотрите использование деревьев выражений для динамических сценариев включений, требующих гибкости во время выполнения.
-
Интегрируйте с шаблоном Repository, включив эти методы расширения в специализированные репозитории для лучшей организации.
-
Следуйте лучшим практикам производительности, используя AsSplitQuery() для сложных отношений и будучи внимательным к избыточной выборке данных.
Подход преобразует многословный, повторяющийся код, как в ваших исходных примерах, в чистые, повторно используемые методы, такие как .IncludeFullBuchungData() и .IncludeKKKontoDetails(), значительно улучшая поддерживаемость, сохраняя всю функциональность ваших исходных запросов.