Как избежать дублирования кода при использовании метода .Include Entity Framework в нескольких классах?
Я работаю с Entity Framework и у меня есть несколько классов, которым требуются похожие цепочки .Include и .ThenInclude. Например:
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(), который можно использовать в разных классах для избежания такого дублирования?
Каковы лучшие практики организации запросов Entity Framework для минимизации дублирования кода при сохранении читаемости и производительности?
Вы можете избежать дублирования кода при использовании метода .Include в Entity Framework, создаваемые повторно используемые методы расширения и эффективно организуя логику запросов. Наиболее распространенный подход включает создание методов расширения, которые инкапсулируют общие шаблоны включения, позволяя повторно использовать одни и те же цепи навигации в нескольких запросах без дублирования кода.
Содержание
- Методы расширения для общих шаблонов включения
- Шаблон репозитория с повторно используемыми включениями
- Вспомогательные классы DbContext
- Сторонние решения
- Лучшие практики организации
- Рассмотрения производительности
Методы расширения для общих шаблонов включения
Наиболее прямое решение вашей проблемы - создание методов расширения, которые инкапсулируют ваши общие цепи включения. Этот подход позволяет повторно использовать одни и те же шаблоны навигации в разных запросах DbSet.
Базовый подход с методами расширения
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);
}
}
Пример использования
// Вместо длинной цепочки используйте ваш метод расширения
var query = currentDBContext.FBBuchungenCollection
.IncludeStandardBuchungIncludes()
.Where(x => x.SomeCondition);
// Для KKKonto
var kontoQuery = currentDBContext.KKKontoCollection
.IncludeStandardKontoIncludes()
.Where(x => x.SomeCondition);
Модульный подход с композируемыми расширениями
Для лучшей организации вы можете разбить ваши включения на более мелкие, композируемые методы:
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, поскольку это позволяет составлять вместе несколько повторно используемых запросов.”
Шаблон репозитория с повторно используемыми включениями
Другой подход - реализация шаблона репозитория, который инкапсулирует общую логику включения:
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
Вы можете создавать вспомогательные классы, которые динамически генерируют выражения включения:
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. Иерархическая организация
Организуйте ваши включения иерархически:
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 для построения сложных запросов:
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 для идентификации запросов:
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. Используйте селективные включения
Создайте несколько методов включения для разных сценариев:
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. Мониторинг производительности запросов
Используйте логирование для мониторинга ваших запросов:
// В вашем DbContext
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information)
.EnableSensitiveDataLogging();
}
Источники
- r/dotnet на Reddit: Как повторно использовать запросы в .NET Core с Entity Framework без “громоздкого” репозитория?
- StackOverflow: Как написать метод репозитория для .ThenInclude в EF Core 2
- StackOverflow: Entity Framework Core - Использование методов расширения внутри Queryable
- Microsoft Learn: Метод QueryableExtensions.Include (EF6)
- Microsoft Learn: Метод EntityFrameworkQueryableExtensions.Include (EF Core)
- GitHub - ClaveConsulting/Expressionify: Использование методов расширения в запросах Entity Framework Core
- Обзор Entity Framework Extensions
Заключение
Чтобы избежать дублирования кода при использовании метода .Include в Entity Framework, рассмотрите эти ключевые стратегии:
- Создавайте методы расширения для общих шаблонов включения, позволяя повторно использовать одни и те же цепи навигации в нескольких запросах
- Используйте шаблон репозитория для централизации логики включения и поддержания разделения ответственности
- Реализуйте иерархическую организацию включений с базовыми, детальными и сложными уровнями
- Рассмотрите сторонние библиотеки, такие как NeinLinq, для расширенных возможностей построения запросов
- Используйте маркированные запросы для лучшей отладки и мониторинга
- Будьте избирательны в отношении включений, чтобы избежать проблем с производительностью
Подход с методами расширения обычно является наиболее прямым и сохраняет читаемость, устраняя дублирование. Начните с простых повторно используемых методов включения и постепенно создавайте более сложные композируемые шаблоны по мере роста вашего приложения. Помните о мониторинге производительности запросов и корректировке вашей стратегии включения на основе фактических шаблонов использования.