팁 (조언)
이 콘텐츠는 .NET Docs 또는 오프라인으로 읽을 수 있는 다운로드 가능한 무료 PDF로 제공되는 컨테이너화된 .NET 애플리케이션용 .NET 마이크로 서비스 아키텍처인 eBook에서 발췌한 내용입니다.
SQL Server, Oracle 또는 PostgreSQL과 같은 관계형 데이터베이스를 사용하는 경우 EF(Entity Framework)를 기반으로 지속성 계층을 구현하는 것이 좋습니다. EF는 LINQ를 지원하고 모델에 강력한 형식의 개체를 제공하고 데이터베이스에 대한 지속성을 간소화합니다.
Entity Framework는 .NET Framework의 일부로 오랜 역사를 가지고 있습니다. .NET을 사용하는 경우 .NET과 동일한 방식으로 Windows 또는 Linux에서 실행되는 Entity Framework Core도 사용해야 합니다. EF Core는 훨씬 더 작은 공간과 중요한 성능 향상으로 구현된 Entity Framework의 완전한 재작성입니다.
Entity Framework Core 소개
EF(Entity Framework) Core는 인기 있는 Entity Framework 데이터 액세스 기술의 가볍고 확장 가능하며 플랫폼 간 버전입니다. 2016년 중반에 .NET Core와 함께 도입되었습니다.
EF Core에 대한 소개는 Microsoft 설명서에서 이미 사용할 수 있으므로 여기서는 해당 정보에 대한 링크만 제공합니다.
추가 리소스
엔티티 프레임워크 코어
https://learn.microsoft.com/ef/core/Visual Studio를 사용하여 ASP.NET Core 및 Entity Framework Core 시작
https://learn.microsoft.com/aspnet/core/data/ef-mvc/DbContext 클래스
https://learn.microsoft.com/dotnet/api/microsoft.entityframeworkcore.dbcontextEF Core 및 EF6.x 비교
https://learn.microsoft.com/ef/efcore-and-ef6/index
DDD 관점에서 Entity Framework Core의 인프라
DDD 관점에서 EF의 중요한 기능은 EF 용어에서 POCO 코드 우선 엔터티라고도 하는 POCO 도메인 엔터티를 사용하는 기능입니다. POCO 도메인 엔터티를 사용하는 경우 도메인 모델 클래스는 지속성 무시 및 인프라 무지 원칙에 따라 지속성 무지입니다.
DDD 패턴별로 엔터티 클래스 자체 내에서 도메인 동작 및 규칙을 캡슐화해야 하므로 컬렉션에 액세스할 때 고정, 유효성 검사 및 규칙을 제어할 수 있습니다. 따라서 DDD에서 자식 엔터티 또는 값 객체의 컬렉션에 대한 공용 액세스를 허용하는 것은 권장되는 실천이 아닙니다. 대신, 필드 및 속성 컬렉션을 업데이트할 수 있는 방법과 시기 및 해당 동작이 발생할 때 발생해야 하는 동작 및 동작을 제어하는 메서드를 노출하려고 합니다.
EF Core 1.1부터 이러한 DDD 요구 사항을 충족하기 위해 공용 속성 대신 엔터티에 일반 필드를 가질 수 있습니다. 엔터티 필드에 외부에서 액세스할 수 없도록 하려면 속성 대신 특성 또는 필드를 만들면 됩니다. 개인 속성 설정자를 사용할 수도 있습니다.
마찬가지로, 이제 지속성을 위해 EF를 사용하는 엔터티의 컬렉션(예IReadOnlyCollection<T>
: )에 대한 프라이빗 필드 멤버가 백업하는 형식의 List<T>
공용 속성을 사용하여 컬렉션에 대한 읽기 전용 액세스를 가질 수 있습니다. 이전 버전의 Entity Framework에서는 컬렉션 속성을 지원 ICollection<T>
해야 했습니다. 즉, 부모 엔터티 클래스를 사용하는 개발자는 속성 컬렉션을 통해 항목을 추가하거나 제거할 수 있었습니다. 이러한 가능성은 DDD의 권장 패턴에 위배됩니다.
다음 코드 예제와 같이 읽기 전용 IReadOnlyCollection<T>
개체를 노출하는 동안 프라이빗 컬렉션을 사용할 수 있습니다.
public class Order : Entity
{
// Using private fields, allowed since EF Core 1.1
private DateTime _orderDate;
// Other fields ...
private readonly List<OrderItem> _orderItems;
public IReadOnlyCollection<OrderItem> OrderItems => _orderItems;
protected Order() { }
public Order(int buyerId, int paymentMethodId, Address address)
{
// Initializations ...
}
public void AddOrderItem(int productId, string productName,
decimal unitPrice, decimal discount,
string pictureUrl, int units = 1)
{
// Validation logic...
var orderItem = new OrderItem(productId, productName,
unitPrice, discount,
pictureUrl, units);
_orderItems.Add(orderItem);
}
}
이 속성은 OrderItems
.를 사용하여 IReadOnlyCollection<OrderItem>
읽기 전용으로만 액세스할 수 있습니다. 이 형식은 읽기 전용이므로 일반 외부 업데이트로부터 보호됩니다.
EF Core는 도메인 모델을 "오염"하지 않고 도메인 모델을 실제 데이터베이스에 매핑하는 방법을 제공합니다. 매핑 작업이 지속성 계층에서 구현되기 때문에 순수 .NET POCO 코드입니다. 해당 매핑 작업에서 필드-데이터베이스 매핑을 구성해야 합니다. 다음 예제에서는 OnModelCreating
의 OrderingContext
메서드와 OrderEntityTypeConfiguration
클래스에서 호출이 EF Core에 해당 필드를 통해 SetPropertyAccessMode
속성에 접근하도록 지시합니다.
// At OrderingContext.cs from eShopOnContainers
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// ...
modelBuilder.ApplyConfiguration(new OrderEntityTypeConfiguration());
// Other entities' configuration ...
}
// At OrderEntityTypeConfiguration.cs from eShopOnContainers
class OrderEntityTypeConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> orderConfiguration)
{
orderConfiguration.ToTable("orders", OrderingContext.DEFAULT_SCHEMA);
// Other configuration
var navigation =
orderConfiguration.Metadata.FindNavigation(nameof(Order.OrderItems));
//EF access the OrderItem collection property through its backing field
navigation.SetPropertyAccessMode(PropertyAccessMode.Field);
// Other configuration
}
}
속성 대신 필드를 사용하는 경우 엔터티는 속성 OrderItem
이 있는 List<OrderItem>
것처럼 유지됩니다. 그러나 주문에 새 항목을 추가하기 위한 단일 접근자 메서드인 AddOrderItem
를 제공합니다. 따라서 동작과 데이터가 함께 연결되며 도메인 모델을 사용하는 모든 애플리케이션 코드 전체에서 일관성이 유지됩니다.
Entity Framework Core를 사용하여 사용자 지정 리포지토리 구현
구현 수준에서 리포지토리는 다음 클래스와 같이 업데이트를 수행할 때 작업 단위(EF Core의 DBContext)에 의해 조정된 데이터 지속성 코드가 있는 클래스일 뿐입니다.
// using directives...
namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Repositories
{
public class BuyerRepository : IBuyerRepository
{
private readonly OrderingContext _context;
public IUnitOfWork UnitOfWork
{
get
{
return _context;
}
}
public BuyerRepository(OrderingContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public Buyer Add(Buyer buyer)
{
return _context.Buyers.Add(buyer).Entity;
}
public async Task<Buyer> FindAsync(string buyerIdentityGuid)
{
var buyer = await _context.Buyers
.Include(b => b.Payments)
.Where(b => b.FullName == buyerIdentityGuid)
.SingleOrDefaultAsync();
return buyer;
}
}
}
인터페이스는 IBuyerRepository
도메인 모델 계층에서 계약으로 제공됩니다. 그러나 리포지토리 구현은 지속성 및 인프라 계층에서 수행됩니다.
EF DbContext는 종속성 주입을 통해 생성자를 통해 제공됩니다. IoC 컨테이너의 기본 수명(ServiceLifetime.Scoped
으로 명시적으로 설정할 수 있음) 덕분에 동일한 HTTP 요청 범위 내의 여러 리포지토리 간에 공유됩니다.
리포지토리에서 구현하는 메서드(업데이트 또는 트랜잭션 및 쿼리)
각 리포지토리 클래스 내에서 관련 집계에 포함된 엔터티의 상태를 업데이트하는 지속성 메서드를 배치해야 합니다. 집계와 관련 리포지토리 사이에는 일대일 관계가 있음을 기억하세요. 집계 루트 엔터티 개체에는 EF 그래프 내에 포함된 자식 엔터티가 있을 수 있습니다. 예를 들어 구매자는 관련 자식 엔터티로 여러 결제 방법을 사용할 수 있습니다.
eShopOnContainers의 마이크로 서비스 정렬 방식도 CQS/CQRS를 기반으로 하기 때문에 대부분의 쿼리는 사용자 지정 리포지토리에서 구현되지 않습니다. 개발자는 집계, 집계당 사용자 지정 리포지토리 및 일반적으로 DDD에 의해 부과되는 제한 없이 프레젠테이션 계층에 필요한 쿼리 및 조인을 자유롭게 만들 수 있습니다. 이 가이드에서 제안하는 대부분의 사용자 지정 리포지토리에는 여러 업데이트 또는 트랜잭션 메서드가 있지만 데이터를 업데이트하는 데 필요한 쿼리 메서드만 있습니다. 예를 들어 BuyerRepository 리포지토리는 주문과 관련된 새 구매자를 만들기 전에 애플리케이션이 특정 구매자가 존재하는지 여부를 알아야 하기 때문에 FindAsync 메서드를 구현합니다.
그러나 프레젠테이션 계층 또는 클라이언트 앱으로 보낼 데이터를 가져오는 실제 쿼리 메서드는 Dapper를 사용하는 유연한 쿼리를 기반으로 CQRS 쿼리에서 설명한 대로 구현됩니다.
사용자 지정 저장소 사용 대 EF DbContext 직접 사용
Entity Framework DbContext 클래스는 작업 단위 및 리포지토리 패턴을 기반으로 하며 ASP.NET Core MVC 컨트롤러와 같은 코드에서 직접 사용할 수 있습니다. 작업 단위 및 리포지토리 패턴은 eShopOnContainers의 CRUD 카탈로그 마이크로 서비스에서와 같이 가장 간단한 코드를 생성합니다. 가능한 가장 간단한 코드를 원하는 경우 많은 개발자처럼 DbContext 클래스를 직접 사용할 수 있습니다.
그러나 사용자 지정 리포지토리를 구현하면 더 복잡한 마이크로 서비스 또는 애플리케이션을 구현할 때 몇 가지 이점이 있습니다. 작업 단위 및 리포지토리 패턴은 애플리케이션 및 도메인 모델 계층에서 분리되도록 인프라 지속성 계층을 캡슐화하기 위한 것입니다. 이러한 패턴을 구현하면 모의 리포지토리를 사용하여 데이터베이스에 대한 액세스를 시뮬레이션할 수 있습니다.
그림 7-18에서는 리포지토리를 사용하지 않는 것과(EF DbContext를 직접 사용) 리포지토리를 사용하는 것과 리포지토리를 사용하는 것의 차이점을 확인할 수 있으므로 해당 리포지토리를 더 쉽게 모의할 수 있습니다.
그림 7-18. 사용자 지정 리포지토리 및 일반 DbContext 사용
그림 7-18은 사용자 지정 리포지토리를 사용하면 리포지토리를 모의하여 테스트를 용이하게 하는 데 사용할 수 있는 추상화 계층을 추가한다는 것을 보여줍니다. 모의할 때는 여러 가지 대안이 있습니다. 리포지토리만 조롱하거나 전체 작업 단위를 조롱할 수 있습니다. 일반적으로 리포지토리만 조롱하는 것만으로는 충분하며 전체 작업 단위를 추상화하고 모의하는 복잡성은 일반적으로 필요하지 않습니다.
나중에 애플리케이션 계층에 초점을 맞추면 ASP.NET Core에서 종속성 주입이 작동하는 방식과 리포지토리를 사용할 때 구현되는 방법을 확인할 수 있습니다.
즉, 사용자 지정 리포지토리를 사용하면 데이터 계층 상태의 영향을 받지 않는 단위 테스트를 통해 코드를 보다 쉽게 테스트할 수 있습니다. Entity Framework를 통해 실제 데이터베이스에 액세스하는 테스트를 실행하는 경우 단위 테스트가 아니라 통합 테스트이며 훨씬 느립니다.
DbContext를 직접 사용하는 경우 단위 테스트를 위해 예측 가능한 데이터가 있는 메모리 내 SQL Server를 사용하여 모의 또는 단위 테스트를 실행해야 합니다. 그러나 DbContext를 모의하거나 가짜 데이터를 제어하려면 리포지토리 수준에서 모의하는 것보다 더 많은 작업이 필요합니다. 물론 항상 MVC 컨트롤러를 테스트할 수 있습니다.
IoC 컨테이너의 EF DbContext 및 IUnitOfWork 인스턴스 수명
DbContext
객체(IUnitOfWork
객체로 노출됨)는 동일한 HTTP 요청 범위 내에서 여러 리포지토리 간에 공유되어야 합니다. 예를 들어 실행 중인 작업이 여러 집계를 처리해야 하거나 단순히 여러 리포지토리 인스턴스를 사용 중이기 때문에 마찬가지입니다. 또한 인터페이스는 EF Core 형식이 아니라 도메인 계층의 일부임을 IUnitOfWork
언급하는 것이 중요합니다.
이렇게 하려면 개체 인스턴스의 DbContext
서비스 수명을 ServiceLifetime.Scoped로 설정해야 합니다. ASP.NET Core Web API 프로젝트에서 IoC 컨테이너에 DbContext
을(를) builder.Services.AddDbContext
와(과) 함께 등록할 때, Program.cs 파일의 기본 수명입니다. 다음 코드에서는 이를 보여 줍니다.
// Add framework services.
builder.Services.AddMvc(options =>
{
options.Filters.Add(typeof(HttpGlobalExceptionFilter));
}).AddControllersAsServices();
builder.Services.AddEntityFrameworkSqlServer()
.AddDbContext<OrderingContext>(options =>
{
options.UseSqlServer(Configuration["ConnectionString"],
sqlOptions => sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().
Assembly.GetName().Name));
},
ServiceLifetime.Scoped // Note that Scoped is the default choice
// in AddDbContext. It is shown here only for
// pedagogic purposes.
);
DbContext 인스턴스화 모드는 ServiceLifetime.Transient 또는 ServiceLifetime.Singleton으로 구성해서는 안 됩니다.
IoC 컨테이너의 리포지토리 인스턴스 수명
비슷한 방식으로 리포지토리의 수명은 일반적으로 스코프(Autofac의 InstancePerLifetimeScope)로 설정되어야 합니다. 일시적일 수도 있지만(Autofac의 InstancePerDependency), 범위가 지정된 수명을 사용하는 경우 메모리와 관련하여 서비스가 더 효율적입니다.
// Registering a Repository in Autofac IoC container
builder.RegisterType<OrderRepository>()
.As<IOrderRepository>()
.InstancePerLifetimeScope();
리포지토리에 싱글톤 생애 주기를 사용하는 경우, DbContext가 범위(InstancePerLifetimeScope) 생애 주기(DBContext의 기본 생애 주기)로 설정되어 있을 때, 심각한 동시성 문제가 발생할 수 있습니다. 리포지토리와 DbContext의 서비스 수명이 모두 스코프일 경우, 이러한 문제를 피할 수 있습니다.
추가 리소스
ASP.NET MVC 애플리케이션에서 리포지토리 및 작업 단위 패턴 구현
https://www.asp.net/mvc/overview/older-versions/getting-started-with-ef-5-using-mvc-4/implementing-the-repository-and-unit-of-work-patterns-in-an-asp-net-mvc-application조나단 앨런. Entity Framework, Dapper 및 Chain을 사용하여 리포지토리 패턴에 대한 구현 전략
https://www.infoq.com/articles/repository-implementation-strategies세자르 드 라 토레. ASP.NET Core IoC 컨테이너 서비스 수명을 Autofac IoC 컨테이너 인스턴스 범위와 비교
https://devblogs.microsoft.com/cesardelatorre/comparing-asp-net-core-ioc-service-life-times-and-autofac-ioc-instance-scopes/
테이블 매핑
테이블 매핑은 쿼리하여 데이터베이스에 저장할 테이블 데이터를 식별합니다. 이전에는 도메인 엔터티(예: 제품 또는 주문 도메인)를 사용하여 관련 데이터베이스 스키마를 생성하는 방법을 알아보았습니다. EF는 규칙의 개념을 중심으로 강력하게 설계되었습니다. 규칙은 "테이블 이름은 무엇인가요?" 또는 "기본 키는 무엇인가요?" 등의 질문을 다룹니다. 규칙은 일반적으로 기존 이름을 기반으로 합니다. 예를 들어 기본 키는 .로 끝나는 Id
속성이 되는 것이 일반적입니다.
규칙에 따라 각 엔터티는 파생 컨텍스트에서 엔터티를 노출하는 속성과 이름이 같은 DbSet<TEntity>
테이블에 매핑되도록 설정됩니다.
DbSet<TEntity>
지정된 엔터티에 대해 값이 제공되지 않으면 클래스 이름이 사용됩니다.
데이터 주석과 Fluent API 비교
많은 추가 EF Core 규칙이 있으며 대부분의 EF Core 규칙은 OnModelCreating 메서드 내에서 구현된 데이터 주석 또는 Fluent API를 사용하여 변경할 수 있습니다.
데이터 주석은 DDD 관점에서 보다 방해가 되는 방식으로 엔터티 모델 클래스 자체에서 사용해야 합니다. 인프라 데이터베이스와 관련된 데이터 주석으로 모델을 오염하기 때문입니다. 반면에 Fluent API는 데이터 지속성 인프라 계층 내에서 대부분의 규칙 및 매핑을 변경하는 편리한 방법이므로 엔터티 모델은 지속성 인프라에서 정리되고 분리됩니다.
Fluent API 및 OnModelCreating 메서드
설명한 대로 규칙 및 매핑을 변경하기 위해 DbContext 클래스에서 OnModelCreating 메서드를 사용할 수 있습니다.
eShopOnContainers의 정렬 마이크로 서비스는 다음 코드와 같이 필요한 경우 명시적 매핑 및 구성을 구현합니다.
// At OrderingContext.cs from eShopOnContainers
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// ...
modelBuilder.ApplyConfiguration(new OrderEntityTypeConfiguration());
// Other entities' configuration ...
}
// At OrderEntityTypeConfiguration.cs from eShopOnContainers
class OrderEntityTypeConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> orderConfiguration)
{
orderConfiguration.ToTable("orders", OrderingContext.DEFAULT_SCHEMA);
orderConfiguration.HasKey(o => o.Id);
orderConfiguration.Ignore(b => b.DomainEvents);
orderConfiguration.Property(o => o.Id)
.UseHiLo("orderseq", OrderingContext.DEFAULT_SCHEMA);
//Address value object persisted as owned entity type supported since EF Core 2.0
orderConfiguration
.OwnsOne(o => o.Address, a =>
{
a.WithOwner();
});
orderConfiguration
.Property<int?>("_buyerId")
.UsePropertyAccessMode(PropertyAccessMode.Field)
.HasColumnName("BuyerId")
.IsRequired(false);
orderConfiguration
.Property<DateTime>("_orderDate")
.UsePropertyAccessMode(PropertyAccessMode.Field)
.HasColumnName("OrderDate")
.IsRequired();
orderConfiguration
.Property<int>("_orderStatusId")
.UsePropertyAccessMode(PropertyAccessMode.Field)
.HasColumnName("OrderStatusId")
.IsRequired();
orderConfiguration
.Property<int?>("_paymentMethodId")
.UsePropertyAccessMode(PropertyAccessMode.Field)
.HasColumnName("PaymentMethodId")
.IsRequired(false);
orderConfiguration.Property<string>("Description").IsRequired(false);
var navigation = orderConfiguration.Metadata.FindNavigation(nameof(Order.OrderItems));
// DDD Patterns comment:
//Set as field (New since EF 1.1) to access the OrderItem collection property through its field
navigation.SetPropertyAccessMode(PropertyAccessMode.Field);
orderConfiguration.HasOne<PaymentMethod>()
.WithMany()
.HasForeignKey("_paymentMethodId")
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
orderConfiguration.HasOne<Buyer>()
.WithMany()
.IsRequired(false)
.HasForeignKey("_buyerId");
orderConfiguration.HasOne(o => o.OrderStatus)
.WithMany()
.HasForeignKey("_orderStatusId");
}
}
동일한 OnModelCreating
메서드 내에서 모든 Fluent API 매핑을 설정할 수 있지만, 예제와 같이 해당 코드를 분할하고 엔터티당 하나씩 여러 구성 클래스를 사용하는 것이 좋습니다. 특히 대형 모델의 경우 서로 다른 엔터티 형식을 구성하기 위한 별도의 구성 클래스를 사용하는 것이 좋습니다.
예제의 코드는 몇 가지 명시적 선언 및 매핑을 보여 줍니다. 그러나 EF Core 규칙은 이러한 매핑을 대부분 자동으로 수행하므로 경우에 필요한 실제 코드는 더 작을 수 있습니다.
EF Core의 Hi/Lo 알고리즘
앞의 예제에서 코드의 흥미로운 측면은 Hi/Lo 알고리즘 을 키 생성 전략으로 사용한다는 것입니다.
Hi/Lo 알고리즘은 변경 내용을 커밋하기 전에 고유 키가 필요한 경우에 유용합니다. 요약하자면, Hi-Lo 알고리즘은 데이터베이스에 행을 즉시 저장하지 않고 테이블 행에 고유 식별자를 할당합니다. 이렇게 하면 일반 순차 데이터베이스 ID와 마찬가지로 식별자 사용을 즉시 시작할 수 있습니다.
Hi/Lo 알고리즘은 관련 데이터베이스 시퀀스에서 고유 ID의 일괄 처리를 가져오는 메커니즘을 설명합니다. 이러한 ID는 데이터베이스가 고유성을 보장하므로 사용자 간에 충돌이 발생하지 않으므로 사용하기에 안전합니다. 이 알고리즘은 다음과 같은 이유로 흥미롭습니다.
작업 단위 패턴이 깨지지 않습니다.
데이터베이스로의 왕복을 최소화하기 위해 일괄 처리로 시퀀스 ID를 가져옵니다.
GUID를 사용하는 기술과 달리 사람이 읽을 수 있는 식별자를 생성합니다.
EF Core는 앞의 예제와 같이 메서드를 사용하여 UseHiLo
를 지원합니다.
속성 대신 필드 매핑
EF Core 1.1부터 사용할 수 있는 이 기능을 사용하면 열을 필드에 직접 매핑할 수 있습니다. 엔터티 클래스의 속성을 사용하지 않고 테이블에서 필드로 열을 매핑할 수 있습니다. 일반적으로 엔터티 외부에서 액세스할 필요가 없는 내부 상태에 대한 프라이빗 필드가 사용됩니다.
단일 필드 또는 필드와 같은 List<>
컬렉션을 사용하여 이 작업을 수행할 수 있습니다. 이 점은 앞에서 도메인 모델 클래스 모델링에 대해 설명했지만 여기서는 이전 코드에서 강조 표시된 구성을 PropertyAccessMode.Field
사용하여 매핑이 수행되는 방법을 확인할 수 있습니다.
인프라 수준에서 숨겨진 EF Core의 섀도 속성 사용
EF Core의 섀도 속성은 엔터티 클래스 모델에 존재하지 않는 속성입니다. 이러한 속성의 값과 상태는 인프라 수준의 ChangeTracker 클래스에서만 유지됩니다.
쿼리 사양 패턴 구현
디자인 섹션의 앞부분에서 소개한 것처럼 쿼리 사양 패턴은 선택적 정렬 및 페이징 논리를 사용하여 쿼리 정의를 배치할 수 있는 위치로 설계된 Domain-Driven 디자인 패턴입니다.
쿼리 사양 패턴은 개체의 쿼리를 정의합니다. 예를 들어 일부 제품을 검색하는 페이징된 쿼리를 캡슐화하기 위해 필요한 입력 매개 변수(pageNumber, pageSize, filter 등)를 사용하는 PagedProduct 사양을 만들 수 있습니다. 그런 다음 리포지토리 메서드(일반적으로 List() 오버로드) 내에서 IQuerySpecification을 수락하고 해당 사양에 따라 예상 쿼리를 실행합니다.
제네릭 사양 인터페이스의 예는 eShopOnWeb 참조 애플리케이션에서 사용되는 코드와 유사한 다음 코드입니다.
// GENERIC SPECIFICATION INTERFACE
// https://github.com/dotnet-architecture/eShopOnWeb
public interface ISpecification<T>
{
Expression<Func<T, bool>> Criteria { get; }
List<Expression<Func<T, object>>> Includes { get; }
List<string> IncludeStrings { get; }
}
그런 다음 제네릭 사양 기본 클래스의 구현은 다음과 같습니다.
// GENERIC SPECIFICATION IMPLEMENTATION (BASE CLASS)
// https://github.com/dotnet-architecture/eShopOnWeb
public abstract class BaseSpecification<T> : ISpecification<T>
{
public BaseSpecification(Expression<Func<T, bool>> criteria)
{
Criteria = criteria;
}
public Expression<Func<T, bool>> Criteria { get; }
public List<Expression<Func<T, object>>> Includes { get; } =
new List<Expression<Func<T, object>>>();
public List<string> IncludeStrings { get; } = new List<string>();
protected virtual void AddInclude(Expression<Func<T, object>> includeExpression)
{
Includes.Add(includeExpression);
}
// string-based includes allow for including children of children
// for example, Basket.Items.Product
protected virtual void AddInclude(string includeString)
{
IncludeStrings.Add(includeString);
}
}
다음 사양은 장바구니의 ID 또는 장바구니가 속한 구매자의 ID를 고려하여 단일 바구니 엔터티를 로드합니다. 그것은 바구니의 컬렉션을 적극적으로 로드할 것입니다.
// SAMPLE QUERY SPECIFICATION IMPLEMENTATION
public class BasketWithItemsSpecification : BaseSpecification<Basket>
{
public BasketWithItemsSpecification(int basketId)
: base(b => b.Id == basketId)
{
AddInclude(b => b.Items);
}
public BasketWithItemsSpecification(string buyerId)
: base(b => b.BuyerId == buyerId)
{
AddInclude(b => b.Items);
}
}
마지막으로, 제네릭 EF 리포지토리에서 이러한 사양을 사용하여 지정된 엔터티 형식 T와 관련된 데이터를 필터링하고 즉시 로드하는 방법을 아래에서 확인할 수 있습니다.
// GENERIC EF REPOSITORY WITH SPECIFICATION
// https://github.com/dotnet-architecture/eShopOnWeb
public IEnumerable<T> List(ISpecification<T> spec)
{
// fetch a Queryable that includes all expression-based includes
var queryableResultWithIncludes = spec.Includes
.Aggregate(_dbContext.Set<T>().AsQueryable(),
(current, include) => current.Include(include));
// modify the IQueryable to include any string-based include statements
var secondaryResult = spec.IncludeStrings
.Aggregate(queryableResultWithIncludes,
(current, include) => current.Include(include));
// return the result of the query using the specification's criteria expression
return secondaryResult
.Where(spec.Criteria)
.AsEnumerable();
}
필터링 논리를 캡슐화하는 것 외에도 사양은 채울 속성을 포함하여 반환할 데이터의 모양을 지정할 수 있습니다.
리포지토리에서 반환 IQueryable
하는 것은 권장되지 않지만 리포지토리 내에서 이를 사용하여 결과 집합을 빌드하는 것은 매우 좋습니다. 위의 List 메서드에서 사용되는 이 방법을 확인할 수 있습니다. 이 방법은 중간 IQueryable
식을 사용하여 마지막 줄의 사양 조건을 사용하여 쿼리를 실행하기 전에 쿼리의 포함 목록을 작성합니다.
eShopOnWeb 샘플에서 사양 패턴이 적용되는 방법을 알아봅니다.
추가 리소스
테이블 매핑
https://learn.microsoft.com/ef/core/modeling/relational/tablesHiLo를 사용하여 Entity Framework Core를 사용하여 키 생성
https://www.talkingdotnet.com/use-hilo-to-generate-keys-with-entity-framework-core/기초 필드
https://learn.microsoft.com/ef/core/modeling/backing-field스티브 스미스. Entity Framework Core의 캡슐화된 컬렉션
https://ardalis.com/encapsulated-collections-in-entity-framework-core그림자 속성
https://learn.microsoft.com/ef/core/modeling/shadow-properties사양 패턴
https://deviq.com/specification-pattern/Ardalis.Specification NuGet 패키지 eShopOnWeb에서 사용됩니다. \ https://www.nuget.org/packages/Ardalis.Specification
.NET