Table of Contents

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?> or Task<TEntityView?>
  • Behavior when not found: Returns null if the entity is not found
  • Recommended usage: Use when entity existence is uncertain

GetAsync

  • Non-nullable return: Returns Task<TEntity> or Task<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 FindAsync when:

    • Entity existence is uncertain
    • You want to avoid exceptions and explicitly handle the "not found" case
    • You implement conditional logic based on existence
  • Use GetAsync when:

    • 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

  • GetAsync and FindAsync: First check the entity cache (change tracker) before querying the database
  • GetListByKeysAsync: Retrieves cached entities when available, only queries the database for missing entities

Direct database query methods

  • GetQuery: Always returns a fresh queryable that bypasses cache
  • GetListAsync: Always executes a new database query
  • GetPagedListAsync: 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 FindAsync for nullable returns, GetAsync when 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 GetState method
  • Use database profiling tools to identify performance bottlenecks

See also