Repositories
Overview
The repository pattern in Neos provides a standardized way to access and manipulate data, abstracting the underlying data access logic. Neos implements two main types of repositories:
- Entity repositories (
IRepository<TEntity>) - For domain entities - Entity view repositories (
IEntityViewRepository<TEntityView>) - For entity views used in application layer
This separation follows the principles of clean architecture, where domain entities represent business logic and entity views represent application-specific data projections.
Note
The SQL snippets in this article are representative translations of the repository calls and LINQ expressions. The exact SQL generated at runtime depends on the database provider, entity view definition, and query composition.
Important
When you need the exact SQL emitted by your application, use the live monitoring features of the Manager. During development, the Manager lets you inspect recorded requests, nested operations, and the SQL queries generated at runtime.
Entity repositories
Interface
The IRepository<TEntity> interface provides core CRUD operations for domain entities that inherit from BusinessEntity.
Accessing the original value from a base type
The non-generic IRepository interface exposes BusinessEntity GetOriginal(BusinessEntity entity), which returns the original (pre-modification) state of any entity. It delegates to the generic IRepository<TEntity>.GetOriginal, so you can read the original value of an entity even when you only hold it as a BusinessEntity.
To obtain the concrete repository for an arbitrary entity instance, inject the IEntityRepositoryAccessor service. It resolves the right IRepository from any BusinessEntity, walking its type hierarchy. This is what makes it possible to access the original value (and other repository operations) from a rule declared on a base or abstract entity, where the concrete type is only known at runtime.
Usage example
public class OrderService
{
private readonly IRepository<Order> _orderRepository;
private readonly IUnitOfWork _unitOfWork;
public OrderService(IRepository<Order> orderRepository, IUnitOfWork unitOfWork)
{
_orderRepository = orderRepository;
_unitOfWork = unitOfWork;
}
/// <summary>
/// Creates a new order.
/// </summary>
/// <param name="request">The request containing order details.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>The created order.</returns>
public async Task<Order> CreateOrderAsync(CreateOrderRequest request, CancellationToken cancellationToken = default)
{
Order order = _orderRepository.AddNew();
order.CustomerId = request.CustomerId;
order.OrderDate = DateTime.UtcNow;
order.Status = OrderStatus.Pending;
Result result = await _unitOfWork.SaveAsync(cancellationToken);
if (result.IsFailed)
{
throw new BusinessException(string.Join(Environment.NewLine, result.Errors.Select(e => e.Message)));
}
return order;
}
/// <summary>
/// Gets an order by its identifier.
/// </summary>
/// <param name="orderId">The identifier of the order.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>The order if found; otherwise, an exception is thrown.</returns>
public async Task<Order> GetOrderByIdAsync(int orderId, CancellationToken cancellationToken = default)
{
return await _orderRepository.GetAsync(orderId, cancellationToken);
}
/// <summary>
/// Updates the status of an order.
/// </summary>
/// <param name="orderId">The identifier of the order.</param>
/// <param name="newStatus">The new status to set.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>The result of the update operation.</returns>
public async Task<Result> UpdateOrderStatusAsync(int orderId, OrderStatus newStatus, CancellationToken cancellationToken = default)
{
Order order = await GetOrderByIdAsync(orderId, cancellationToken);
order.Status = newStatus;
order.UpdatedDate = DateTime.UtcNow;
return await _unitOfWork.SaveAsync(cancellationToken);
}
/// <summary>
/// Gets orders for a specific customer.
/// </summary>
/// <param name="customerId">The identifier of the customer.</param>
/// <returns>A queryable collection of orders for the specified customer.</returns>
public IQueryable<Order> GetOrdersByCustomer(int customerId)
{
return _orderRepository.GetQuery()
.Where(o => o.CustomerId == customerId)
.OrderByDescending(o => o.OrderDate);
}
/// <summary>
/// Gets paginated orders for the current year.
/// </summary>
/// <param name="pageIndex">The page index (one-based).</param>
/// <param name="pageSize">The number of orders per page.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>A paginated list of orders for the current year.</returns>
public async Task<PaginatedList<Order>> GetPaginatedOrdersForCurrentYearAsync(int pageIndex, int pageSize, CancellationToken cancellationToken = default)
{
DateTime startOfYear = new DateTime(DateTime.UtcNow.Year, 1, 1);
return _orderRepository.GetQuery()
.Where(o => o.OrderDate >= startOfYear)
.OrderByDescending(o => o.OrderDate)
.Skip((pageIndex - 1) * pageSize)
.Take(pageSize);
}
}
Representative SQL equivalents
CreateOrderAsync
INSERT INTO Orders (CustomerId, OrderDate, Status)
VALUES (@CustomerId, @OrderDate, @Status);
GetOrderByIdAsync
SELECT o.Id,
o.CustomerId,
o.OrderDate,
o.Status,
o.UpdatedDate
FROM Orders AS o
WHERE o.Id = @OrderId;
UpdateOrderStatusAsync
UPDATE Orders
SET Status = @NewStatus,
UpdatedDate = @UpdatedDate
WHERE Id = @OrderId;
GetOrdersByCustomer
SELECT o.Id,
o.CustomerId,
o.OrderDate,
o.Status,
o.UpdatedDate
FROM Orders AS o
WHERE o.CustomerId = @CustomerId
ORDER BY o.OrderDate DESC;
GetPaginatedOrdersForCurrentYearAsync
SELECT o.Id,
o.CustomerId,
o.OrderDate,
o.Status,
o.UpdatedDate
FROM Orders AS o
WHERE o.OrderDate >= @StartOfYear
ORDER BY o.OrderDate DESC
OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY;
Entity view repositories
Interface
The IEntityViewRepository<TEntityView> interface is designed for application layer data access, providing additional functionality for entity views.
Usage example
public class OrderViewService
{
private readonly IEntityViewRepository<IOrderView> _orderViewRepository;
private readonly IUnitOfWork _unitOfWork;
public OrderViewService(
IEntityViewRepository<IOrderView> orderViewRepository,
IUnitOfWork unitOfWork)
{
_orderViewRepository = orderViewRepository;
_unitOfWork = unitOfWork;
}
/// <summary>
/// Finds an order view by its identifier.
/// </summary>
/// <param name="orderId">The identifier of the order.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>The order view if found; otherwise, null.</returns>
public async Task<IOrderView?> FindOrderViewAsync(int orderId, CancellationToken cancellationToken = default)
{
return await _orderViewRepository.FindAsync(orderId, cancellationToken);
}
/// <summary>
/// Gets recent orders within the specified number of days.
/// </summary>
/// <param name="days">The number of days to look back for recent orders.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>A list of recent order views.</returns>
public async Task<IEnumerable<IOrderView>> GetRecentOrdersAsync(int days = 30, CancellationToken cancellationToken = default)
{
DateTime cutoffDate = DateTime.UtcNow.AddDays(-days);
return await _orderViewRepository.GetListAsync(
query => query.Where(o => o.OrderDate >= cutoffDate)
.OrderByDescending(o => o.OrderDate)
.Take(100),
cancellationToken);
}
/// <summary>
/// Counts the number of active orders.
/// </summary>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>The count of active orders.</returns>
public async Task<long> CountActiveOrdersAsync(CancellationToken cancellationToken = default)
{
return await _orderViewRepository.CountAsync(
query => query.Where(o => o.Status == OrderStatus.Active), cancellationToken);
}
/// <summary>
/// Creates a new order from a JSON representation.
/// </summary>
/// <param name="orderJson">The JSON string representing the order.</param>
/// <param name="cancellationToken">Token to cancel the operation.</param>
/// <returns>The created order view.</returns>
public async Task<IOrderView> CreateOrderFromJsonAsync(string orderJson, CancellationToken cancellationToken = default)
{
IOrderView orderView = await _orderViewRepository.AddFromJsonAsync(orderJson, cancellationToken);
Result result = await _unitOfWork.SaveAsync(cancellationToken);
if (result.IsFailed)
{
throw new BusinessException(string.Join(Environment.NewLine, result.Errors.Select(e => e.Message)));
}
return orderView;
}
}
Representative SQL equivalents
FindOrderViewAsync
SELECT ov.Id,
ov.CustomerId,
ov.OrderDate,
ov.Status
FROM OrderView AS ov
WHERE ov.Id = @OrderId;
GetRecentOrdersAsync
SELECT ov.Id,
ov.CustomerId,
ov.OrderDate,
ov.Status
FROM OrderView AS ov
WHERE ov.OrderDate >= @CutoffDate
ORDER BY ov.OrderDate DESC
FETCH FIRST 100 ROWS ONLY;
CountActiveOrdersAsync
SELECT COUNT(*)
FROM OrderView AS ov
WHERE ov.Status = @ActiveStatus;
CreateOrderFromJsonAsync
INSERT INTO Orders (CustomerId, OrderDate, Status)
VALUES (@CustomerId, @OrderDate, @Status);
AddFromJsonAsync first materializes the entity view from the JSON payload, then persists the underlying data during SaveAsync(). When the entity view spans multiple tables, the persistence phase can generate multiple INSERT and UPDATE statements rather than a single SQL command.
Query transformation
When you need aggregations, groupings, or custom projections (e.g. revenue per category, headcount per department), use GetTransformedListAsync instead of loading full entity views and processing them in memory.
See Query transformation for details and examples.
Entity view repository context
Entity view repositories can operate within a specific execution context that defines filtering conditions and additional data retrieval options. This context is particularly important when working with server-side methods that need to apply the same filters as the calling UI view.
For detailed information on how to configure and use entity view repository context, see the detailed documentation.
FindAsync vs GetAsync
An important distinction exists between the FindAsync and GetAsync methods in both repository types (IRepository<TEntity> and IEntityViewRepository<TEntityView>).
Behavioral differences
FindAsync
- Nullable return: Returns
Task<TEntity?>orTask<TEntityView?> - Behavior when not found: Returns
nullif the entity is not found - Recommended usage: Use when entity existence is uncertain
GetAsync
- Non-nullable return: Returns
Task<TEntity>orTask<TEntityView> - Behavior when not found: Throws an exception if the entity is not found
- Recommended usage: Use when entity existence is expected
Usage recommendations
Use
FindAsyncwhen:- Entity existence is uncertain
- You want to avoid exceptions and explicitly handle the "not found" case
- You implement conditional logic based on existence
Use
GetAsyncwhen:- You are certain the entity exists
- You want the code to fail fast if the entity doesn't exist
- The absence of the entity constitutes an error state in your business logic
Cache behavior
An important performance characteristic to understand is how different repository methods interact with the entity cache.
Cache-aware methods
GetAsyncandFindAsync: First check the entity cache (change tracker) before querying the databaseGetListByKeysAsync: Retrieves cached entities when available, only queries the database for missing entities
Direct database query methods
GetQuery: Always returns a fresh queryable that bypasses cacheGetListAsync: Always executes a new database queryGetPagedListAsync: Always executes a new database query
Note
Use GetAsync/FindAsync when you need a single entity and want to benefit from caching. Use GetListAsync or GetQuery when you need fresh data from the database or want to apply complex filtering.
Bulk operations
Neos provides support for bulk operations through Entity Framework's ExecuteUpdateAsync and ExecuteDeleteAsync methods, which allow you to perform mass updates and deletes directly in the database without loading entities into memory.
Update
The ExecuteUpdateAsync method allows you to update multiple records in a single database operation:
// Update all orders from a specific customer
await _orderRepository.GetQuery()
.Where(o => o.CustomerId == customerId)
.ExecuteUpdateAsync(setter => setter
.SetProperty(o => o.Status, OrderStatus.Cancelled)
.SetProperty(o => o.UpdatedDate, DateTime.UtcNow), cancellationToken);
Representative SQL:
UPDATE Orders
SET Status = @CancelledStatus,
UpdatedDate = @UpdatedDate
WHERE CustomerId = @CustomerId;
Delete
The ExecuteDeleteAsync method allows you to delete multiple records without loading them first:
// Delete all expired sessions
_sessionRepository.GetQuery()
.Where(s => s.ExpirationDate < DateTime.UtcNow)
.ExecuteDeleteAsync(cancellationToken);
Representative SQL:
DELETE FROM Sessions
WHERE ExpirationDate < @UtcNow;
Key considerations
- Performance: These operations are executed directly in the database, providing better performance for large datasets
- Change tracking bypass: These operations bypass Entity Framework's change tracking, so they don't trigger entity events
- Transaction integration: These operations integrate seamlessly with the unit of work pattern and respect transaction boundaries
- Immediate execution: Unlike other LINQ operations, these execute immediately and don't wait for
SaveAsync()
Warning
Since ExecuteUpdateAsync and ExecuteDeleteAsync bypass change tracking, they won't trigger entity validation rules, saving events, or other Entity Framework interceptors. Use them carefully in contexts where business logic depends on these mechanisms.
Best practices
Repository usage guidelines
Use appropriate repository type
// Use entity repository for domain logic
public class OrderDomainService
{
private readonly IOrderRepository _orderRepository;
private readonly IUnitOfWork _unitOfWork;
public async Task<Result> ProcessOrderAsync(int orderId, CancellationToken cancellationToken = default)
{
// Domain business logic
Order order = await _orderRepository.GetAsync(orderId, cancellationToken);
order.CalculateTotal();
order.ApplyDiscount();
return await _unitOfWork.SaveAsync(cancellationToken);
}
}
// Use entity view repository for application logic
public class OrderApplicationService
{
private readonly IOrderViewRepository _orderViewRepository;
private readonly IOrderHasChangedNotification _orderHasChangedNotification;
public async Task NotifyOrderChangedAsync(int orderId, CancellationToken cancellationToken = default)
{
IOrderView orderView = await _orderViewRepository.GetAsync(orderId, cancellationToken);
await _orderHasChangedNotification.SendToAllConnectionsAsync(cancellationToken);
}
}
Use cancellation tokens
Always propagate cancellation tokens through your repository calls to enable proper request cancellation and improve application responsiveness.
// Proper cancellation token propagation
public class OrderService
{
private readonly IOrderRepository _orderRepository;
private readonly IOrderDetailRepository _orderDetailRepository;
private readonly INotificationService _notificationService;
private readonly IUnitOfWork _unitOfWork;
public async Task ProcessOrderAsync(int orderId, CancellationToken cancellationToken = default)
{
// Propagate cancellation token to all async operations
Order order = await _orderRepository.GetAsync(orderId, cancellationToken);
List<OrderDetail> orderDetails = await _orderDetailRepository.GetQuery()
.Where(od => od.OrderId == orderId)
.ToListAsync(cancellationToken);
// Process details
foreach (OrderDetail orderDetail in orderDetails)
{
cancellationToken.ThrowIfCancellationRequested();
await ProcessDetailAsync(orderDetail, cancellationToken);
}
await _notificationService.NotifyOrderProcessedAsync(orderId, cancellationToken);
}
private async Task<Result> ProcessDetailAsync(OrderDetail detail, CancellationToken cancellationToken)
{
// Heavy processing that can be cancelled
await SomeHeavyOperationAsync(detail, cancellationToken);
Result result = await _unitOfWork.SaveAsync(cancellationToken);
return result;
}
}
Performance considerations
Use async methods
// Always use async methods for database operations
IReadOnlyList<IOrderView> orders = await _orderViewRepository.GetListAsync(cancellationToken: cancellationToken);
// DON'T: Avoid blocking synchronous calls
// this blocks the calling thread, prevents scalability, wastes server resources
List<Order> orders = _orderRepository.GetAll().ToList(); // Blocking synchronous call
Optimize queries
// Use specific includes
return _orderRepository.GetQuery()
.Include(o => o.Customer)
.Include(o => o.OrderDetails.Where(od => od.IsActive));
Implement pagination
// Implement pagination for large datasets
await _orderViewRepository.GetPagedListAsync(100, 50, cancellationToken);
Troubleshooting
Common issues
- Entity not found: Use
FindAsyncfor nullable returns,GetAsyncwhen you expect the entity to exist - Performance issues: Use query customization and projection instead of loading full entities
Debugging tips
- Use SQL profiling to monitor generated queries and transaction behavior with the Manager
- Check entity states with
GetStatemethod - Use database profiling tools to identify performance bottlenecks