The Repository Pattern provides an abstraction layer between the domain/business logic and data persistence, centralizing data access logic and improving testability, maintainability, and flexibility. Intent Architect implements this pattern through the Intent.EntityFrameworkCore.Repositories module.
Repository Benefits
Repositories encapsulate data access logic, provide a collection-like interface for domain objects, and enable easy switching between different data access strategies without affecting business logic.
Intent Architect enables lazy loading by default through EF Core’s lazy loading proxies:
var customer = await _customerRepository.FindByIdAsync(customerId);// Orders are loaded automatically when accessedforeach (var order in customer.Orders) // Database query happens here{ Console.WriteLine(order.OrderNumber);}
Configuration:
Setting: Database Settings - Lazy loading with proxies
Load related entities upfront to avoid multiple queries:
Override CreateQuery
Custom Query Method
Override Base Method
Query Options Parameter
Configure repository to always eager load specific relationships:
public class CustomerRepository : RepositoryBase<Customer, Customer, ApplicationDbContext>, ICustomerRepository{ public CustomerRepository(ApplicationDbContext dbContext) : base(dbContext) { } // All queries will include Orders protected override IQueryable<Customer> CreateQuery() { var result = base.CreateQuery(); return result.Include(c => c.Orders); }}
In the Domain Designer, use composition (black diamond) to model owned relationships:
This results in:
Only Order gets a repository
OrderItem is accessed through the Order aggregate
Saving Order saves all its OrderItems
// Load the aggregate rootvar order = await _orderRepository.FindByIdAsync(orderId);// Modify owned entities through the aggregateorder.AddItem(productId, quantity, unitPrice);order.RemoveItem(itemId);// Save the entire aggregate at once_orderRepository.Update(order);await _unitOfWork.SaveChangesAsync();
Sometimes you need direct access to owned entities for performance reasons:
1
Apply Repository Stereotype
In the Domain Designer, apply the Repository stereotype to the owned entity
2
Repository is Generated
Intent Architect will generate both interface and implementation:
public interface IOrderItemRepository : IEFRepository<OrderItem, OrderItem>{ // Custom query methods}
Accessing owned entities directly bypasses aggregate consistency boundaries. Use this only for non-functional requirements like performance optimization.
Encapsulate query logic in reusable specifications:
public class ActiveCustomersSpecification{ public static Expression<Func<Customer, bool>> Criteria => c => c.Status == CustomerStatus.Active && !c.IsDeleted;}public class HighValueCustomersSpecification{ public static Expression<Func<Customer, bool>> Criteria => c => c.TotalPurchases > 10000m;}
Repositories should focus on data access, not business logic:
// Good - Simple data accessTask<Customer?> FindByEmailAsync(string email);// Bad - Business logic in repositoryTask<bool> IsCustomerEligibleForDiscountAsync(Guid customerId);
Put business logic in domain entities or domain services instead.
Return Domain Objects
Repositories should return domain entities, not DTOs:
// Good - Returns domain entityTask<Customer?> FindByIdAsync(Guid id);// Bad - Returns DTOTask<CustomerDto> FindByIdAsync(Guid id);
Map to DTOs in the application layer (handlers/services).
One Repository Per Aggregate Root
Don’t create repositories for every entity:
// Good - Repository for aggregate rootpublic interface IOrderRepository { }// Bad - Repository for owned entitypublic interface IOrderItemRepository { } // Access through Order instead
Use Async Methods
Always use asynchronous methods for I/O operations: