IronAlpine.Framework.Data 1.0.1

There is a newer version of this package available.
See the version list below for details.
dotnet add package IronAlpine.Framework.Data --version 1.0.1
                    
NuGet\Install-Package IronAlpine.Framework.Data -Version 1.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="IronAlpine.Framework.Data" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="IronAlpine.Framework.Data" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="IronAlpine.Framework.Data" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add IronAlpine.Framework.Data --version 1.0.1
                    
#r "nuget: IronAlpine.Framework.Data, 1.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package IronAlpine.Framework.Data@1.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=IronAlpine.Framework.Data&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=IronAlpine.Framework.Data&version=1.0.1
                    
Install as a Cake Tool

IronAlpine.Data v4.0.0

Complete Data Access and Repository Pattern Implementation Guide

NuGet Package: IronAlpine.Data

Author: IronAlpine Framework Team
License: MIT
Repository: IronAlpineFramework/IronAlpine
Target Frameworks: .NET 9.0, .NET 10.0


Table of Contents

  1. Overview
  2. Core Concepts
  3. Installation
  4. Repository Pattern
  5. Unit of Work Pattern
  6. EF Core Integration
  7. Domain Event Dispatch
  8. Auditing Behavior
  9. Transaction Handling
  10. Specification Pattern
  11. Redis Caching Integration
  12. Query Optimization
  13. Complete API Reference
  14. Best Practices
  15. Common Scenarios
  16. Troubleshooting
  17. Performance Considerations
  18. Migration from v3

Overview

IronAlpine.Data is a comprehensive data access layer framework built on Entity Framework Core that implements the Repository and Unit of Work patterns. It provides:

  • Repository Pattern: Clean abstraction over database access
  • Unit of Work: Transactional consistency across multiple repositories
  • Domain Event Dispatch: Automatic publishing of domain events
  • Auditing: Comprehensive audit trail for all changes
  • Soft Deletes: Logical deletion support
  • Optimistic Concurrency: Automatic concurrency conflict detection
  • Entity Versioning: Track entity version changes
  • Redis Caching: Integrated distributed caching
  • Query Specifications: Reusable, composable query logic
  • EF Core Integration: Seamless integration with Entity Framework Core

Core Concepts

Entity Framework Core Foundation

IronAlpine.Data builds on EF Core with:

  • Custom model contributions for soft deletes, versioning, auditing
  • Interceptors for automatic timestamp management
  • Domain event dispatch support
  • Automatic audit log creation

Repository Pattern Benefits

Application
    ↓
    ├─ IRepository<TEntity>
    │     ↓
    │  EF Core DbContext
    │     ↓
    │  Database

The Repository pattern provides:

  1. Abstraction: Hide database details from business logic
  2. Testability: Mock repositories in unit tests
  3. Consistency: Standardized query interface
  4. Flexibility: Change ORM without affecting application

Unit of Work Pattern

Coordinates multiple repositories to ensure transactional consistency:

UnitOfWork
├─ Repository<User>
├─ Repository<Order>
├─ Repository<Payment>
└─ SaveChangesAsync() → Single transaction

Installation

NuGet Package Manager

dotnet add package IronAlpine.Data --version 4.0.0
dotnet add package IronAlpine.Data.EFCore --version 4.0.0
dotnet add package IronAlpine.Data.EFCore.Mediator --version 4.0.0

Package Manager Console

Install-Package IronAlpine.Data -Version 4.0.0
Install-Package IronAlpine.Data.EFCore -Version 4.0.0
Install-Package IronAlpine.Data.EFCore.Mediator -Version 4.0.0

Packages

  1. IronAlpine.Data: Repository and Unit of Work abstractions
  2. IronAlpine.Data.EFCore: EF Core implementation
  3. IronAlpine.Data.EFCore.Mediator: Mediator integration (UoW behavior, transaction handling, auditing)

Repository Pattern

Repository Interfaces

IReadRepository<TEntity>

Read-only operations:

public interface IReadRepository<TEntity, TId>
    where TEntity : class, IEntity<TId>
    where TId : notnull
{
    IQueryable<TEntity> AsQueryable();
    IQueryable<TEntity> AsNoTracking();
    IQueryable<TEntity> IgnoreQueryFilters();

    Task<TEntity?> FindAsync(TId id, CancellationToken ct = default);
    Task<TEntity?> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken ct = default);
    Task<bool> AnyAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken ct = default);
    Task<int> CountAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken ct = default);

    IAsyncEnumerable<TEntity> AsAsyncEnumerable();
}

public interface IReadRepository<TEntity> : IReadRepository<TEntity, Guid>
    where TEntity : class, IEntity<Guid>
{
}

Usage:

public class GetUserByIdQueryHandler : IRequestHandler<GetUserByIdQuery, UserResponse>
{
    private readonly IReadRepository<User> _userRepository;

    public GetUserByIdQueryHandler(IReadRepository<User> userRepository)
    {
        _userRepository = userRepository;
    }

    public async Task<UserResponse> Handle(GetUserByIdQuery request, CancellationToken ct)
    {
        // Direct FindAsync
        var user = await _userRepository.FindAsync(request.UserId, ct);
        if (user is null)
            throw new UserNotFoundException(request.UserId);

        return MapToResponse(user);
    }
}
IWriteRepository<TEntity>

Write operations:

public interface IWriteRepository<TEntity, TId>
    where TEntity : class, IEntity<TId>
    where TId : notnull
{
    Task AddAsync(TEntity entity, CancellationToken ct = default);
    Task AddRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default);

    Task UpdateAsync(TEntity entity, CancellationToken ct = default);
    Task UpdateRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default);

    Task DeleteAsync(TEntity entity, CancellationToken ct = default);
    Task DeleteRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default);

    Task DeleteByIdAsync(TId id, CancellationToken ct = default);
    Task DeleteRangeByIdAsync(IEnumerable<TId> ids, CancellationToken ct = default);

    Task SoftDeleteAsync(TEntity entity, CancellationToken ct = default);
    Task SoftDeleteRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default);

    Task<bool> ExistsAsync(TId id, CancellationToken ct = default);
}

public interface IWriteRepository<TEntity> : IWriteRepository<TEntity, Guid>
    where TEntity : class, IEntity<Guid>
{
}

Usage:

public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, Result<UserResponse>>
{
    private readonly IWriteRepository<User> _userRepository;
    private readonly IUnitOfWork _unitOfWork;

    public CreateUserCommandHandler(
        IWriteRepository<User> userRepository,
        IUnitOfWork unitOfWork)
    {
        _userRepository = userRepository;
        _unitOfWork = unitOfWork;
    }

    public async Task<Result<UserResponse>> Handle(CreateUserCommand request, CancellationToken ct)
    {
        // Create entity
        var user = new User
        {
            Id = Guid.NewGuid(),
            Name = request.Name,
            Email = request.Email,
            CreatedAt = DateTime.UtcNow
        };

        // Add to repository
        await _userRepository.AddAsync(user, ct);

        // Save via Unit of Work
        await _unitOfWork.SaveChangesAsync(ct);

        return Result.Success(MapToResponse(user));
    }
}
IRepository<TEntity>

Combined read and write:

public interface IRepository<TEntity, TId> :
    IReadRepository<TEntity, TId>,
    IWriteRepository<TEntity, TId>
    where TEntity : class, IEntity<TId>
    where TId : notnull
{
}

public interface IRepository<TEntity> : IRepository<TEntity, Guid>
    where TEntity : class, IEntity<Guid>
{
}
IAggregateRepository<TAggregate>

For DDD aggregates (entities with domain logic):

public interface IAggregateRepository<TAggregate, TId> :
    IAggregateReadRepository<TAggregate, TId>,
    IAggregateWriteRepository<TAggregate, TId>
    where TAggregate : class, IEntity<TId>
    where TId : notnull
{
}

public interface IAggregateReadRepository<TAggregate, TId>
    where TAggregate : class, IEntity<TId>
    where TId : notnull
{
    IQueryable<TAggregate> AsQueryable();
    Task<TAggregate?> FindAsync(TId id, CancellationToken ct = default);
    Task<TAggregate?> FirstOrDefaultAsync(Expression<Func<TAggregate, bool>> predicate, CancellationToken ct = default);
}

public interface IAggregateWriteRepository<TAggregate, TId>
    where TAggregate : class, IEntity<TId>
    where TId : notnull
{
    Task AddAsync(TAggregate aggregate, CancellationToken ct = default);
    Task UpdateAsync(TAggregate aggregate, CancellationToken ct = default);
    Task DeleteAsync(TAggregate aggregate, CancellationToken ct = default);
}

Usage with DDD:

public class Order : IEntity<Guid>
{
    public Guid Id { get; set; }
    public Guid UserId { get; set; }
    public decimal TotalAmount { get; set; }
    public OrderStatus Status { get; set; } = OrderStatus.Pending;
    public List<OrderItem> Items { get; set; } = [];
    public DateTime CreatedAt { get; set; }

    // Domain logic
    public void AddItem(Product product, int quantity)
    {
        if (Status != OrderStatus.Pending)
            throw new InvalidOperationException("Cannot add items to non-pending order");

        if (quantity <= 0)
            throw new ArgumentException("Quantity must be positive");

        var existingItem = Items.FirstOrDefault(i => i.ProductId == product.Id);
        if (existingItem is not null)
        {
            existingItem.Quantity += quantity;
        }
        else
        {
            Items.Add(new OrderItem
            {
                ProductId = product.Id,
                Quantity = quantity,
                UnitPrice = product.Price
            });
        }

        RecalculateTotal();
    }

    public void Checkout()
    {
        if (Items.Count == 0)
            throw new InvalidOperationException("Cannot checkout empty order");

        Status = OrderStatus.CheckedOut;
    }

    private void RecalculateTotal()
    {
        TotalAmount = Items.Sum(i => i.UnitPrice * i.Quantity);
    }
}

public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Result<OrderResponse>>
{
    private readonly IAggregateRepository<Order> _orderRepository;
    private readonly IReadRepository<Product> _productRepository;
    private readonly IUnitOfWork _unitOfWork;

    public async Task<Result<OrderResponse>> Handle(CreateOrderCommand request, CancellationToken ct)
    {
        // Create aggregate
        var order = new Order { Id = Guid.NewGuid(), UserId = request.UserId };

        // Apply domain logic
        foreach (var item in request.Items)
        {
            var product = await _productRepository.FindAsync(item.ProductId, ct);
            if (product is null)
                return Result.Failure<OrderResponse>($"Product {item.ProductId} not found");

            order.AddItem(product, item.Quantity);
        }

        // Save aggregate
        await _orderRepository.AddAsync(order, ct);
        await _unitOfWork.SaveChangesAsync(ct);

        return Result.Success(MapToResponse(order));
    }
}
IProjectionRepository<TProjection>

Read-model repositories for denormalized views:

public interface IProjectionRepository<TProjection, TId> :
    IProjectionReadRepository<TProjection, TId>,
    IProjectionWriteRepository<TProjection, TId>
    where TProjection : class, IEntity<TId>
    where TId : notnull
{
}

public interface IProjectionReadRepository<TProjection, TId>
{
    IQueryable<TProjection> AsQueryable();
    IQueryable<TProjection> AsNoTracking();
    Task<TProjection?> FindAsync(TId id, CancellationToken ct = default);
}

public interface IProjectionWriteRepository<TProjection, TId>
{
    Task AddAsync(TProjection projection, CancellationToken ct = default);
    Task AddRangeAsync(IEnumerable<TProjection> projections, CancellationToken ct = default);
    Task UpdateAsync(TProjection projection, CancellationToken ct = default);
    Task UpdateRangeAsync(IEnumerable<TProjection> projections, CancellationToken ct = default);
    Task DeleteAsync(TProjection projection, CancellationToken ct = default);
    Task DeleteRangeAsync(IEnumerable<TProjection> projections, CancellationToken ct = default);
}

Usage with event projections:

// Read model for dashboard
public class UserDashboardProjection : IEntity<Guid>
{
    public Guid Id { get; set; } // UserId
    public string UserName { get; set; }
    public int OrderCount { get; set; }
    public decimal TotalSpent { get; set; }
    public DateTime LastOrderDate { get; set; }
}

// Event handler updating projection
public class OrderCreatedProjectionHandler : INotificationHandler<OrderCreatedNotification>
{
    private readonly IProjectionRepository<UserDashboardProjection> _dashboardRepository;
    private readonly IUnitOfWork _unitOfWork;

    public async Task Handle(OrderCreatedNotification notification, CancellationToken ct)
    {
        var dashboard = await _dashboardRepository.FindAsync(notification.UserId, ct);

        if (dashboard is null)
        {
            dashboard = new UserDashboardProjection
            {
                Id = notification.UserId,
                UserName = notification.UserName,
                OrderCount = 1,
                TotalSpent = notification.OrderTotal,
                LastOrderDate = notification.CreatedAt
            };
            await _dashboardRepository.AddAsync(dashboard, ct);
        }
        else
        {
            dashboard.OrderCount++;
            dashboard.TotalSpent += notification.OrderTotal;
            dashboard.LastOrderDate = notification.CreatedAt;
            await _dashboardRepository.UpdateAsync(dashboard, ct);
        }

        await _unitOfWork.SaveChangesAsync(ct);
    }
}

Unit of Work Pattern

IUnitOfWork Interface

public interface IUnitOfWork
{
    Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}

Complete Example

public class TransferFundsCommandHandler : IRequestHandler<TransferFundsCommand, Result>
{
    private readonly IRepository<Account> _accountRepository;
    private readonly IRepository<Transaction> _transactionRepository;
    private readonly IUnitOfWork _unitOfWork;
    private readonly IEventBus _eventBus;

    public TransferFundsCommandHandler(
        IRepository<Account> accountRepository,
        IRepository<Transaction> transactionRepository,
        IUnitOfWork unitOfWork,
        IEventBus eventBus)
    {
        _accountRepository = accountRepository;
        _transactionRepository = transactionRepository;
        _unitOfWork = unitOfWork;
        _eventBus = eventBus;
    }

    public async Task<Result> Handle(TransferFundsCommand request, CancellationToken ct)
    {
        // Load accounts
        var fromAccount = await _accountRepository.FindAsync(request.FromAccountId, ct);
        if (fromAccount is null)
            return Result.Failure("Source account not found");

        var toAccount = await _accountRepository.FindAsync(request.ToAccountId, ct);
        if (toAccount is null)
            return Result.Failure("Destination account not found");

        // Validate business rules
        if (fromAccount.Balance < request.Amount)
            return Result.Failure("Insufficient funds");

        // Apply domain logic
        fromAccount.Withdraw(request.Amount);
        toAccount.Deposit(request.Amount);

        // Create transaction records
        var fromTransaction = new Transaction
        {
            Id = Guid.NewGuid(),
            AccountId = fromAccount.Id,
            Type = TransactionType.Debit,
            Amount = request.Amount,
            Description = $"Transfer to {toAccount.Number}",
            CreatedAt = DateTime.UtcNow
        };

        var toTransaction = new Transaction
        {
            Id = Guid.NewGuid(),
            AccountId = toAccount.Id,
            Type = TransactionType.Credit,
            Amount = request.Amount,
            Description = $"Transfer from {fromAccount.Number}",
            CreatedAt = DateTime.UtcNow
        };

        // Update repositories
        await _accountRepository.UpdateAsync(fromAccount, ct);
        await _accountRepository.UpdateAsync(toAccount, ct);
        await _transactionRepository.AddAsync(fromTransaction, ct);
        await _transactionRepository.AddAsync(toTransaction, ct);

        // Save all changes in single transaction
        try
        {
            await _unitOfWork.SaveChangesAsync(ct);

            // Publish events after successful save
            await _eventBus.PublishAsync(
                new FundsTransferredIntegrationEvent
                {
                    FromAccountId = fromAccount.Id,
                    ToAccountId = toAccount.Id,
                    Amount = request.Amount,
                    Timestamp = DateTime.UtcNow
                },
                ct);

            return Result.Success();
        }
        catch (DbUpdateConcurrencyException)
        {
            return Result.Failure("Account was modified by another user, please try again");
        }
        catch (DbUpdateException ex)
        {
            return Result.Failure($"Database error: {ex.Message}");
        }
    }
}

EF Core Integration

DbContext Setup

using IronAlpine.Data.EFCore;
using IronAlpine.Data.EFCore.Modeling;
using Microsoft.EntityFrameworkCore;

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    public DbSet<User> Users { get; set; }
    public DbSet<Order> Orders { get; set; }
    public DbSet<Product> Products { get; set; }
    public DbSet<UserDashboardProjection> UserDashboards { get; set; }
    public DbSet<AuditLog> AuditLogs { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        // Apply entity configurations
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);

        // Use IronAlpine modeling
        modelBuilder.UseIronAlpineDataEfCoreModeling();
    }
}

Service Registration

// Program.cs
var builder = WebApplicationBuilder.CreateBuilder(args);

// Add DbContext with IronAlpine integration
builder.Services.AddIronAlpineDataEfCoreDbContext<ApplicationDbContext>(
    (sp, optionsBuilder) =>
    {
        var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
        optionsBuilder.UseSqlServer(
            connectionString,
            serverOptions =>
            {
                serverOptions.EnableRetryOnFailure(
                    maxRetryCount: 3,
                    maxRetryDelaySeconds: 5,
                    errorNumbersToAdd: null);
            });
    },
    configure: options =>
    {
        // Configure auditing
        options.Auditing.Enabled = true;
        options.Auditing.LogEventChanges = true;

        // Configure soft deletes
        options.SoftDeletes.Enabled = true;

        // Configure versioning
        options.Versioning.Enabled = true;
    });

// Add mediator behaviors for UoW and transactions
builder.Services.AddIronAlpineDataEfCoreMediator();

var app = builder.Build();

appsettings.json Configuration

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=.;Database=IronAlpine;Integrated Security=true;Encrypt=false;"
  },
  "IronAlpineData": {
    "Auditing": {
      "Enabled": true,
      "LogEventChanges": true,
      "UserIdClaimType": "sub"
    },
    "SoftDeletes": {
      "Enabled": true
    },
    "Versioning": {
      "Enabled": true
    }
  }
}

Entity Configuration

using IronAlpine.Data.EFCore.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

public class UserEntityTypeConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder.HasKey(u => u.Id);

        builder.Property(u => u.Id)
            .ValueGeneratedNever();

        builder.Property(u => u.Name)
            .IsRequired()
            .HasMaxLength(100);

        builder.Property(u => u.Email)
            .IsRequired()
            .HasMaxLength(255);

        builder.Property(u => u.Email)
            .UseCollation("SQL_Latin1_General_CP1_CI_AS"); // Case-insensitive on SQL Server

        builder.HasIndex(u => u.Email)
            .IsUnique()
            .HasDatabaseName("IX_Users_Email");

        builder.Property(u => u.CreatedAt)
            .HasDefaultValueSql("GETUTCDATE()");

        builder.Property(u => u.Version)
            .IsConcurrencyToken();

        // Soft delete configuration
        builder.ConfigureSoftDelete(u => u.DeletedAt);

        // Audit configuration
        builder.ConfigureAuditing(u => u.CreatedBy, u => u.UpdatedBy);
    }
}

Domain Event Dispatch

Domain Event Base

public abstract class DomainEvent : IDomainEvent
{
    public Guid AggregateId { get; protected set; }
    public DateTime OccurredAt { get; protected set; } = DateTime.UtcNow;
    public string EventType => GetType().Name;
}

public interface IDomainEvent
{
    Guid AggregateId { get; }
    DateTime OccurredAt { get; }
    string EventType { get; }
}

Aggregate with Domain Events

public class Order : IEntity<Guid>
{
    public Guid Id { get; set; }
    public Guid UserId { get; set; }
    public List<OrderItem> Items { get; set; } = [];
    public OrderStatus Status { get; set; }

    private readonly List<DomainEvent> _events = [];

    public IReadOnlyList<DomainEvent> GetEvents() => _events.AsReadOnly();
    public void ClearEvents() => _events.Clear();

    public void CreateOrder(Guid userId, List<(Guid ProductId, int Quantity)> items)
    {
        Id = Guid.NewGuid();
        UserId = userId;
        Status = OrderStatus.Created;

        foreach (var (productId, quantity) in items)
        {
            Items.Add(new OrderItem { ProductId = productId, Quantity = quantity });
        }

        _events.Add(new OrderCreatedDomainEvent
        {
            AggregateId = Id,
            UserId = UserId,
            ItemCount = Items.Count,
            TotalAmount = Items.Sum(i => i.UnitPrice * i.Quantity)
        });
    }

    public void Checkout()
    {
        if (Status != OrderStatus.Created)
            throw new InvalidOperationException("Order cannot be checked out");

        Status = OrderStatus.CheckedOut;

        _events.Add(new OrderCheckedOutDomainEvent
        {
            AggregateId = Id,
            CheckoutTime = DateTime.UtcNow
        });
    }

    public void Ship()
    {
        if (Status != OrderStatus.Paid)
            throw new InvalidOperationException("Order cannot be shipped");

        Status = OrderStatus.Shipped;

        _events.Add(new OrderShippedDomainEvent
        {
            AggregateId = Id,
            ShipmentDate = DateTime.UtcNow
        });
    }
}

public class OrderCreatedDomainEvent : DomainEvent
{
    public Guid UserId { get; set; }
    public int ItemCount { get; set; }
    public decimal TotalAmount { get; set; }
}

public class OrderCheckedOutDomainEvent : DomainEvent
{
    public DateTime CheckoutTime { get; set; }
}

public class OrderShippedDomainEvent : DomainEvent
{
    public DateTime ShipmentDate { get; set; }
}

Automatic Domain Event Dispatch

// IronAlpine automatically dispatches domain events after SaveChangesAsync
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Result<OrderResponse>>
{
    private readonly IAggregateRepository<Order> _orderRepository;
    private readonly IUnitOfWork _unitOfWork;
    private readonly IPublisher _mediatorPublisher;

    public async Task<Result<OrderResponse>> Handle(CreateOrderCommand request, CancellationToken ct)
    {
        var order = new Order();
        order.CreateOrder(request.UserId, request.Items);

        await _orderRepository.AddAsync(order, ct);
        
        // SaveChangesAsync automatically:
        // 1. Persists order
        // 2. Extracts domain events
        // 3. Publishes events via mediator
        await _unitOfWork.SaveChangesAsync(ct);

        // Domain events already dispatched!
        // Event handlers have executed

        return Result.Success(MapToResponse(order));
    }
}

// Handle domain events
public class OrderCreatedDomainEventHandler : INotificationHandler<OrderCreatedDomainEvent>
{
    private readonly IEmailService _emailService;
    private readonly IReadRepository<User> _userRepository;

    public async Task Handle(OrderCreatedDomainEvent notification, CancellationToken ct)
    {
        var user = await _userRepository.FindAsync(notification.UserId, ct);
        if (user is null)
            return;

        await _emailService.SendAsync(
            to: user.Email,
            subject: "Order Confirmation",
            body: $"Your order for {notification.ItemCount} items has been received.",
            cancellationToken: ct);
    }
}

Auditing Behavior

Auditing Configuration

builder.Services.AddIronAlpineDataEfCoreDbContext<ApplicationDbContext>(
    configureDbContext,
    configure: options =>
    {
        options.Auditing.Enabled = true;
        options.Auditing.LogEventChanges = true;
        options.Auditing.UserIdClaimType = "sub";
        options.Auditing.UserIdConverterType = typeof(CustomAuditUserIdConverter);
    });

// appsettings.json
{
  "IronAlpineData": {
    "Auditing": {
      "Enabled": true,
      "LogEventChanges": true,
      "UserIdClaimType": "sub"
    }
  }
}

Audit Log Entry Structure

public class AuditLogEntry
{
    public Guid Id { get; set; }
    public Guid UserId { get; set; }
    public string EntityName { get; set; }
    public string OperationType { get; set; } // Create, Update, Delete
    public Guid EntityId { get; set; }
    public string? OldValues { get; set; }
    public string? NewValues { get; set; }
    public DateTime CreatedAt { get; set; }
}

// Entity definition from IronAlpine
public class AuditLog : IEntity<Guid>
{
    public Guid Id { get; set; }
    public Guid UserId { get; set; }
    public string CommandName { get; set; }
    public string EntityName { get; set; }
    public Guid? AuditableEntityId { get; set; }
    public DateTime OccurredOn { get; set; }
    public string? Payload { get; set; }
}

Auditable Command Marking

using IronAlpine.Data.Abstractions.Contracts;

public class UpdateUserCommand : IRequest<Result>, IAuditableCommand
{
    public Guid UserId { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }

    // IAuditableCommand implementation
    public Guid GetAuditableEntityId() => UserId;
}

// Auditing behavior automatically logs
public class AuditingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>, IAuditableCommand
    where TResponse : Result
{
    private readonly IAuditLogWriter _auditLogWriter;
    private readonly ICurrentUser _currentUser;

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        var response = await next();

        if (response.IsSuccess)
        {
            await _auditLogWriter.WriteAsync(new AuditLogEntry
            {
                UserId = _currentUser.UserId,
                EntityName = typeof(TRequest).Name,
                OperationType = "Update",
                EntityId = request.GetAuditableEntityId(),
                Payload = JsonSerializer.Serialize(request),
                CreatedAt = DateTime.UtcNow
            }, cancellationToken);
        }

        return response;
    }
}

Querying Audit Logs

public class GetAuditLogsQueryHandler : IRequestHandler<GetAuditLogsQuery, List<AuditLogDto>>
{
    private readonly IReadRepository<AuditLog> _auditLogRepository;

    public async Task<List<AuditLogDto>> Handle(GetAuditLogsQuery request, CancellationToken ct)
    {
        var logs = await _auditLogRepository.AsQueryable()
            .Where(l => l.UserId == request.UserId || request.UserId == Guid.Empty)
            .Where(l => string.IsNullOrEmpty(request.EntityName) || l.EntityName == request.EntityName)
            .Where(l => l.OccurredOn >= request.FromDate && l.OccurredOn <= request.ToDate)
            .OrderByDescending(l => l.OccurredOn)
            .Select(l => new AuditLogDto
            {
                Id = l.Id,
                UserId = l.UserId,
                EntityName = l.EntityName,
                CommandName = l.CommandName,
                EntityId = l.AuditableEntityId,
                Timestamp = l.OccurredOn,
                Payload = l.Payload
            })
            .ToListAsync(ct);

        return logs;
    }
}

Transaction Handling

Transaction Behavior Modes

public enum TransactionMode
{
    Auto,           // Automatic mode detection
    ResultAware,    // Check ITransactionalResult before commit
    Classic,        // Always commit on success
    None            // No automatic transactions
}

Configuration

builder.Services.AddIronAlpineMediator(config =>
{
    config.RegisterServicesFromAssembly(typeof(Program).Assembly);
});

builder.Services.AddIronAlpineDataEfCoreMediator(options =>
{
    options.Transaction.Mode = TransactionMode.Auto; // Default
    options.Transaction.Isolation = IsolationLevel.ReadCommitted;
});

Result-Aware Transactions

public interface ITransactionalResult
{
    bool IsSuccess { get; }
}

public record Result : ITransactionalResult
{
    public bool IsSuccess { get; init; }
    public IReadOnlyList<string> Errors { get; init; } = [];

    public static Result Success() => new() { IsSuccess = true };
    public static Result Failure(params string[] errors) =>
        new() { IsSuccess = false, Errors = errors };
}

public class UpdateUserCommandHandler : IRequestHandler<UpdateUserCommand, Result>
{
    private readonly IRepository<User> _userRepository;
    private readonly IUnitOfWork _unitOfWork;

    public async Task<Result> Handle(UpdateUserCommand request, CancellationToken ct)
    {
        var user = await _userRepository.FindAsync(request.UserId, ct);
        if (user is null)
            return Result.Failure("User not found");

        user.Name = request.Name;
        user.Email = request.Email;

        await _userRepository.UpdateAsync(user, ct);
        await _unitOfWork.SaveChangesAsync(ct);

        // Result.IsSuccess = true → Transaction commits
        // Result.IsSuccess = false → Transaction rolls back
        return Result.Success();
    }
}

Manual Transaction Control

public class ComplexMultiStepCommandHandler : IRequestHandler<ComplexCommand, Result>
{
    private readonly ApplicationDbContext _dbContext;
    private readonly IUnitOfWork _unitOfWork;

    public async Task<Result> Handle(ComplexCommand request, CancellationToken ct)
    {
        // Already in transaction from TransactionBehavior
        var strategy = _dbContext.Database.CreateExecutionStrategy();

        return await strategy.ExecuteAsync(async token =>
        {
            var currentTransaction = _dbContext.Database.CurrentTransaction;

            try
            {
                // Step 1
                await Step1Async(request, token);

                // Step 2
                var step2Result = await Step2Async(request, token);
                if (!step2Result.IsSuccess)
                {
                    // Can still rollback
                    return Result.Failure(step2Result.Errors.ToArray());
                }

                // Step 3
                await Step3Async(request, token);

                await _unitOfWork.SaveChangesAsync(token);

                return Result.Success();
            }
            catch (Exception ex)
            {
                return Result.Failure(ex.Message);
            }
        }, ct);
    }

    private async Task Step1Async(ComplexCommand request, CancellationToken ct)
    {
        // Implementation
    }

    private async Task<Result> Step2Async(ComplexCommand request, CancellationToken ct)
    {
        // Implementation
        return Result.Success();
    }

    private async Task Step3Async(ComplexCommand request, CancellationToken ct)
    {
        // Implementation
    }
}

Specification Pattern

Specification Base Class

public abstract class Specification<TEntity> where TEntity : class
{
    protected Specification()
    {
        Criteria = x => true;
        IsPagingEnabled = false;
    }

    public Expression<Func<TEntity, bool>> Criteria { get; protected set; }
    public List<Expression<Func<TEntity, object>>> Includes { get; } = [];
    public List<string> IncludeStrings { get; } = [];
    public Expression<Func<TEntity, object>> OrderBy { get; protected set; }
    public Expression<Func<TEntity, object>> OrderByDescending { get; protected set; }

    public int Take { get; private set; }
    public int Skip { get; private set; }
    public bool IsPagingEnabled { get; private set; }

    protected virtual void AddInclude(Expression<Func<TEntity, object>> includeExpression)
    {
        Includes.Add(includeExpression);
    }

    protected virtual void AddInclude(string includeString)
    {
        IncludeStrings.Add(includeString);
    }

    protected virtual void ApplyPaging(int skip, int take)
    {
        Skip = skip;
        Take = take;
        IsPagingEnabled = true;
    }

    protected virtual void ApplyOrderBy(Expression<Func<TEntity, object>> orderByExpression)
    {
        OrderBy = orderByExpression;
    }

    protected virtual void ApplyOrderByDescending(Expression<Func<TEntity, object>> orderByDescendingExpression)
    {
        OrderByDescending = orderByDescendingExpression;
    }
}

Specification Implementations

public class ActiveUsersSpecification : Specification<User>
{
    public ActiveUsersSpecification()
    {
        Criteria = u => u.IsActive && u.DeletedAt == null;
        ApplyOrderBy(u => u.Name);
    }
}

public class UserByEmailSpecification : Specification<User>
{
    public UserByEmailSpecification(string email)
    {
        Criteria = u => u.Email == email && u.DeletedAt == null;
    }
}

public class UserWithOrdersSpecification : Specification<User>
{
    public UserWithOrdersSpecification(Guid userId)
    {
        Criteria = u => u.Id == userId;
        AddInclude(u => u.Orders);
        AddInclude("Orders.Items.Product");
    }
}

public class PaginatedProductsSpecification : Specification<Product>
{
    public PaginatedProductsSpecification(int pageNumber, int pageSize, string? category = null)
    {
        if (!string.IsNullOrEmpty(category))
        {
            Criteria = p => p.Category == category && p.IsActive;
        }
        else
        {
            Criteria = p => p.IsActive;
        }

        ApplyOrderBy(p => p.Name);
        ApplyPaging((pageNumber - 1) * pageSize, pageSize);
    }
}

Specification Evaluator

public static class SpecificationEvaluator<TEntity> where TEntity : class
{
    public static IQueryable<TEntity> GetQuery(
        IQueryable<TEntity> inputQuery,
        Specification<TEntity> specification)
    {
        var query = inputQuery;

        // Apply criteria
        if (specification.Criteria != null)
        {
            query = query.Where(specification.Criteria);
        }

        // Apply includes
        query = specification.Includes.Aggregate(query, (current, include) => current.Include(include));

        // Apply string-based includes
        query = specification.IncludeStrings.Aggregate(query, (current, include) => current.Include(include));

        // Apply ordering
        if (specification.OrderBy != null)
        {
            query = query.OrderBy(specification.OrderBy);
        }

        if (specification.OrderByDescending != null)
        {
            query = query.OrderByDescending(specification.OrderByDescending);
        }

        // Apply paging
        if (specification.IsPagingEnabled)
        {
            query = query.Skip(specification.Skip).Take(specification.Take);
        }

        return query;
    }
}

Using Specifications

public class GetUsersWithOrdersQueryHandler : IRequestHandler<GetUsersWithOrdersQuery, List<UserWithOrdersDto>>
{
    private readonly IRepository<User> _userRepository;

    public async Task<List<UserWithOrdersDto>> Handle(GetUsersWithOrdersQuery request, CancellationToken ct)
    {
        var spec = new UserWithOrdersSpecification(request.UserId);

        var users = await _userRepository.AsQueryable()
            .Where(spec.Criteria)
            .Include(spec.Includes[0])
            .Include(spec.IncludeStrings[0])
            .Select(u => new UserWithOrdersDto
            {
                Id = u.Id,
                Name = u.Name,
                Email = u.Email,
                Orders = u.Orders.Select(o => new OrderSummaryDto
                {
                    Id = o.Id,
                    Total = o.Total,
                    CreatedAt = o.CreatedAt,
                    Items = o.Items.Select(i => new OrderItemDto
                    {
                        ProductName = i.Product.Name,
                        Quantity = i.Quantity,
                        Price = i.Price
                    }).ToList()
                }).ToList()
            })
            .ToListAsync(ct);

        return users;
    }
}

Redis Caching Integration

Configuration

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
    options.InstanceName = "IronAlpine:";
});

// appsettings.json
{
  "ConnectionStrings": {
    "Redis": "localhost:6379"
  }
}

Caching Repository Extension

public static class CachingExtensions
{
    public static async Task<TEntity?> FindAsyncCached<TEntity>(
        this IReadRepository<TEntity> repository,
        Guid id,
        IDistributedCache cache,
        TimeSpan? cacheDuration = null,
        CancellationToken ct = default)
        where TEntity : class, IEntity<Guid>
    {
        var cacheKey = $"entity:{typeof(TEntity).Name}:{id}";

        // Try cache first
        var cachedJson = await cache.GetStringAsync(cacheKey, ct);
        if (!string.IsNullOrEmpty(cachedJson))
        {
            return JsonSerializer.Deserialize<TEntity>(cachedJson);
        }

        // Load from database
        var entity = await repository.FindAsync(id, ct);

        if (entity is not null && cacheDuration.HasValue)
        {
            var options = new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = cacheDuration
            };

            await cache.SetStringAsync(
                cacheKey,
                JsonSerializer.Serialize(entity),
                options,
                ct);
        }

        return entity;
    }
}

// Usage
public class GetUserQueryHandler : IRequestHandler<GetUserQuery, UserResponse>
{
    private readonly IReadRepository<User> _userRepository;
    private readonly IDistributedCache _cache;

    public async Task<UserResponse> Handle(GetUserQuery request, CancellationToken ct)
    {
        var user = await _userRepository.FindAsyncCached(
            request.UserId,
            _cache,
            cacheDuration: TimeSpan.FromMinutes(30),
            ct: ct);

        return MapToResponse(user);
    }
}

Cache Invalidation

public class UpdateUserCommandHandler : IRequestHandler<UpdateUserCommand, Result>
{
    private readonly IRepository<User> _userRepository;
    private readonly IUnitOfWork _unitOfWork;
    private readonly IDistributedCache _cache;

    public async Task<Result> Handle(UpdateUserCommand request, CancellationToken ct)
    {
        var user = await _userRepository.FindAsync(request.UserId, ct);
        if (user is null)
            return Result.Failure("User not found");

        user.Name = request.Name;
        user.Email = request.Email;

        await _userRepository.UpdateAsync(user, ct);
        await _unitOfWork.SaveChangesAsync(ct);

        // Invalidate cache
        await _cache.RemoveAsync($"entity:User:{user.Id}", ct);
        await _cache.RemoveAsync($"list:users:active", ct);

        return Result.Success();
    }
}

Query Optimization

Includes and Projections

// GOOD: Load only needed columns
var users = await _userRepository.AsQueryable()
    .Select(u => new UserSummaryDto
    {
        Id = u.Id,
        Name = u.Name,
        Email = u.Email
    })
    .ToListAsync(ct);

// Moderate: Load related entities
var users = await _userRepository.AsQueryable()
    .Include(u => u.Orders)
    .Include(u => u.Profile)
    .Select(u => new UserDetailDto
    {
        Id = u.Id,
        Name = u.Name,
        OrderCount = u.Orders.Count,
        ProfileComplete = u.Profile != null
    })
    .ToListAsync(ct);

// BAD: Load everything
var users = await _userRepository.AsQueryable()
    .ToListAsync(ct); // All columns, no optimization

Paging Implementation

public class PagedQuery : IRequest<PagedResult<T>>
{
    public int PageNumber { get; set; } = 1;
    public int PageSize { get; set; } = 20;
}

public class PagedResult<T>
{
    public List<T> Items { get; set; }
    public int TotalCount { get; set; }
    public int PageNumber { get; set; }
    public int PageSize { get; set; }
    public int TotalPages => (TotalCount + PageSize - 1) / PageSize;
}

public class SearchEntitiesQueryHandler : IRequestHandler<SearchEntitiesQuery, PagedResult<EntityDto>>
{
    private readonly IRepository<Entity> _repository;

    public async Task<PagedResult<EntityDto>> Handle(SearchEntitiesQuery request, CancellationToken ct)
    {
        var query = _repository.AsQueryable()
            .Where(e => string.IsNullOrEmpty(request.Filter) || e.Name.Contains(request.Filter))
            .OrderBy(e => e.Name);

        var totalCount = await query.CountAsync(ct);

        var items = await query
            .Skip((request.PageNumber - 1) * request.PageSize)
            .Take(request.PageSize)
            .Select(e => new EntityDto { Id = e.Id, Name = e.Name })
            .ToListAsync(ct);

        return new PagedResult<EntityDto>
        {
            Items = items,
            TotalCount = totalCount,
            PageNumber = request.PageNumber,
            PageSize = request.PageSize
        };
    }
}

Filtered Queries

// GOOD: Filter in database
var query = _repository.AsQueryable()
    .Where(u => u.IsActive)
    .Where(u => u.CreatedAt > DateTime.UtcNow.AddMonths(-1));

var users = await query.ToListAsync(ct);

// BAD: Filter in memory
var users = await _repository.AsQueryable().ToListAsync(ct);
var activeUsers = users.Where(u => u.IsActive && u.CreatedAt > DateTime.UtcNow.AddMonths(-1)).ToList();

Complete API Reference

IRepository Methods

// Querying
IQueryable<TEntity> AsQueryable();
IQueryable<TEntity> AsNoTracking();
IQueryable<TEntity> IgnoreQueryFilters();

Task<TEntity?> FindAsync(TId id, CancellationToken ct = default);
Task<TEntity?> FirstOrDefaultAsync(
    Expression<Func<TEntity, bool>> predicate,
    CancellationToken ct = default);
Task<bool> AnyAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken ct = default);
Task<int> CountAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken ct = default);

// Writing
Task AddAsync(TEntity entity, CancellationToken ct = default);
Task AddRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default);
Task UpdateAsync(TEntity entity, CancellationToken ct = default);
Task UpdateRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default);
Task DeleteAsync(TEntity entity, CancellationToken ct = default);
Task DeleteRangeAsync(IEnumerable<TEntity> entities, CancellationToken ct = default);
Task DeleteByIdAsync(TId id, CancellationToken ct = default);
Task DeleteRangeByIdAsync(IEnumerable<TId> ids, CancellationToken ct = default);
Task SoftDeleteAsync(TEntity entity, CancellationToken ct = default);
Task<bool> ExistsAsync(TId id, CancellationToken ct = default);

Best Practices

1. One Repository per Aggregate

// GOOD: Separate repositories for aggregates
var orderRepository = serviceProvider.GetRequiredService<IAggregateRepository<Order>>();
var customerRepository = serviceProvider.GetRequiredService<IAggregateRepository<Customer>>();

// Less good: Generic repository for everything
var genericRepository = serviceProvider.GetRequiredService<IRepository<Any>>();

2. Use Async Throughout

// GOOD
public async Task<List<UserDto>> GetUsersAsync(CancellationToken ct)
{
    return await _userRepository.AsQueryable()
        .Select(u => new UserDto { Id = u.Id, Name = u.Name })
        .ToListAsync(ct);
}

// BAD
public List<UserDto> GetUsers()
{
    return _userRepository.AsQueryable()
        .Select(u => new UserDto { Id = u.Id, Name = u.Name })
        .ToList();
}

3. Specify Includes Explicitly

// GOOD: Explicit includes
var user = await _userRepository.AsQueryable()
    .Include(u => u.Orders)
    .Include(u => u.Profile)
    .FirstOrDefaultAsync(u => u.Id == userId, ct);

// BAD: Lazy loading (requires virtual properties)
var user = await _userRepository.FindAsync(userId, ct);
var orders = user.Orders; // Requires lazy loading

4. Cache Appropriately

// GOOD: Cache expensive queries
public async Task<List<UserStatisticsDto>> GetStatisticsAsync(CancellationToken ct)
{
    var cacheKey = "statistics:users:monthly";
    var cached = await _cache.GetStringAsync(cacheKey, ct);
    
    if (!string.IsNullOrEmpty(cached))
        return JsonSerializer.Deserialize<List<UserStatisticsDto>>(cached);

    var stats = await CalculateStatisticsAsync(ct);
    
    await _cache.SetStringAsync(
        cacheKey,
        JsonSerializer.Serialize(stats),
        new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1) },
        ct);

    return stats;
}

// BAD: Don't cache what shouldn't be cached
// Real-time data, small queries, frequently changing data

5. Validate at Repository Boundaries

public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, Result<UserResponse>>
{
    public async Task<Result<UserResponse>> Handle(CreateUserCommand request, CancellationToken ct)
    {
        // Validate input
        if (string.IsNullOrWhiteSpace(request.Email))
            return Result.Failure<UserResponse>("Email is required");

        // Check business rules at repository level
        var existingUser = await _userRepository.FirstOrDefaultAsync(
            u => u.Email == request.Email, ct);
        
        if (existingUser is not null)
            return Result.Failure<UserResponse>("Email already in use");

        // Create entity
        var user = new User { /* ... */ };
        await _userRepository.AddAsync(user, ct);
        await _unitOfWork.SaveChangesAsync(ct);

        return Result.Success(MapToResponse(user));
    }
}

Common Scenarios

Scenario 1: Multi-Entity Transaction with Rollback

public class ProcessPaymentCommandHandler : IRequestHandler<ProcessPaymentCommand, Result>
{
    private readonly IRepository<Account> _accountRepository;
    private readonly IRepository<Transaction> _transactionRepository;
    private readonly IRepository<PaymentNotification> _notificationRepository;
    private readonly IUnitOfWork _unitOfWork;
    private readonly IPaymentGateway _paymentGateway;

    public async Task<Result> Handle(ProcessPaymentCommand request, CancellationToken ct)
    {
        try
        {
            // Load account
            var account = await _accountRepository.FindAsync(request.AccountId, ct);
            if (account is null)
                return Result.Failure("Account not found");

            if (account.Balance < request.Amount)
                return Result.Failure("Insufficient balance");

            // Call external payment service
            var paymentResult = await _paymentGateway.ChargeAsync(
                account.PaymentMethodId,
                request.Amount,
                $"Payment: {request.Description}",
                ct);

            if (!paymentResult.IsSuccessful)
            {
                return Result.Failure($"Payment failed: {paymentResult.ErrorMessage}");
            }

            // Update account
            account.Balance -= request.Amount;
            await _accountRepository.UpdateAsync(account, ct);

            // Record transaction
            var transaction = new Transaction
            {
                Id = Guid.NewGuid(),
                AccountId = account.Id,
                Type = TransactionType.Debit,
                Amount = request.Amount,
                ReferenceId = paymentResult.TransactionId,
                Description = request.Description,
                CreatedAt = DateTime.UtcNow
            };
            await _transactionRepository.AddAsync(transaction, ct);

            // Create notification
            var notification = new PaymentNotification
            {
                Id = Guid.NewGuid(),
                AccountId = account.Id,
                TransactionId = transaction.Id,
                Type = NotificationType.PaymentProcessed,
                IsRead = false,
                CreatedAt = DateTime.UtcNow
            };
            await _notificationRepository.AddAsync(notification, ct);

            // Commit all changes atomically
            await _unitOfWork.SaveChangesAsync(ct);

            return Result.Success();
        }
        catch (DbUpdateConcurrencyException)
        {
            return Result.Failure("Account was modified, please retry");
        }
        catch (DbUpdateException ex)
        {
            return Result.Failure($"Database error: {ex.InnerException?.Message}");
        }
    }
}

Troubleshooting

Q1: "No entity type X found in DbSet"

Problem: Entity not included in DbContext

Solution:

public class ApplicationDbContext : DbContext
{
    public DbSet<User> Users { get; set; }
    public DbSet<Order> Orders { get; set; }  // Add missing entity
}

Q2: Lazy loading not working

Problem: Related entities null even with Include

Solution:

// Ensure Include is called
var user = await _userRepository.AsQueryable()
    .Include(u => u.Orders)  // Don't forget Include!
    .FirstOrDefaultAsync(u => u.Id == userId, ct);

// Or use lazy loading (requires virtual properties)
public class User
{
    public virtual List<Order> Orders { get; set; } = [];  // virtual keyword
}

// Enable lazy loading
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
    options.UseSqlServer(connectionString);
    options.UseLazyLoadingProxies();  // Enable lazy loading
});

Q3: Concurrency conflict when saving

Problem: DbUpdateConcurrencyException on SaveChangesAsync

Solution:

try
{
    await _unitOfWork.SaveChangesAsync(ct);
}
catch (DbUpdateConcurrencyException ex)
{
    // Reload and retry
    var databaseEntry = ex.Entries.Single();
    var proposedValues = databaseEntry.CurrentValues;
    var databaseValues = await databaseEntry.GetDatabaseValuesAsync(ct);
    
    if (databaseValues is null)
    {
        // Entity was deleted
        return Result.Failure("Entity was deleted");
    }

    // Resolve conflict (choose one strategy)
    // 1. Keep database values
    databaseEntry.OriginalValues.SetValues(databaseValues);

    // 2. Or merge intelligently
    // databaseEntry.CurrentValues.SetValues(proposedValues);
    // Manual merge logic

    await _unitOfWork.SaveChangesAsync(ct);
    return Result.Success();
}

Q4: N+1 query problem

Problem: One query per related entity

Solution:

// BAD: N+1 queries
var users = await _userRepository.AsQueryable().ToListAsync(ct);
foreach (var user in users)
{
    var orders = await _orderRepository.AsQueryable()
        .Where(o => o.UserId == user.Id)
        .ToListAsync(ct); // N queries!
}

// GOOD: Single query with Include
var users = await _userRepository.AsQueryable()
    .Include(u => u.Orders)  // Load all at once
    .ToListAsync(ct);

// GOOD: Single query with Select
var userOrders = await _userRepository.AsQueryable()
    .Select(u => new
    {
        User = u,
        Orders = u.Orders
    })
    .ToListAsync(ct);

Q5: Audit logs not recording

Problem: Audit entries not created

Solution:

// Enable auditing in configuration
builder.Services.AddIronAlpineDataEfCoreDbContext<ApplicationDbContext>(
    configureDbContext,
    configure: options =>
    {
        options.Auditing.Enabled = true;  // Must be true
        options.Auditing.LogEventChanges = true;
    });

// Mark command as auditable
public class UpdateUserCommand : IRequest<Result>, IAuditableCommand
{
    public Guid UserId { get; set; }
    public Guid GetAuditableEntityId() => UserId;
}

// Ensure AuditingBehavior is registered
builder.Services.AddIronAlpineMediator(config =>
{
    config.AddOpenBehavior(typeof(AuditingBehavior<,>));
});

Performance Considerations

Query Performance Tips

  1. Use AsNoTracking for read-only queries:
var products = await _productRepository.AsNoTracking()
    .Select(p => new ProductDto { Id = p.Id, Name = p.Name })
    .ToListAsync(ct);
  1. Limit result sets:
var recent = await _orderRepository.AsQueryable()
    .OrderByDescending(o => o.CreatedAt)
    .Take(100)
    .ToListAsync(ct);
  1. Use indexes on frequently filtered columns:
builder.HasIndex(u => u.Email).IsUnique();
builder.HasIndex(u => u.CreatedAt);
  1. Avoid Select with complex calculations:
// BAD: Complex calculation in database
.Select(u => new { 
    u.Id, 
    TaxAmount = u.Income * 0.25 * (u.DependentCount > 3 ? 0.9 : 1.0)
})

// GOOD: Calculate in memory
.Select(u => new { u.Id, u.Income, u.DependentCount })
.AsEnumerable()
.Select(u => new { 
    u.Id, 
    TaxAmount = u.Income * 0.25 * (u.DependentCount > 3 ? 0.9 : 1.0)
})

Migration from v3

Configuration Changes

// v3.0.0
services.AddRepositoryPattern<ApplicationDbContext>();

// v4.0.0
services.AddIronAlpineDataEfCoreDbContext<ApplicationDbContext>(
    (sp, optionsBuilder) =>
    {
        var connectionString = configuration.GetConnectionString("Default");
        optionsBuilder.UseSqlServer(connectionString);
    });

Last Updated: 2026-07-01
Package Version: 4.0.0
License: MIT

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (3)

Showing the top 3 NuGet packages that depend on IronAlpine.Framework.Data:

Package Downloads
IronAlpine.Framework.EventBus

Kafka-backed event bus with outbox/inbox pattern, dead letter support, and replay functionality. Unified namespace: IronAlpine.EventBus. Kafka implementation is internal; only contracts and topology builder are public. SSL cert parameters fully configurable (SslCaLocation, SslCertLocation, SslKeyLocation, SslKeyPassword).

IronAlpine.Framework.Security

JWT authentication, claim-based authorization, and EFCore permission store for IronAlpine microservices. Provides ICurrentUser, permission policy provider, security builder, and public authorization store contracts.

IronAlpine.Framework.Caching

Distributed caching for IronAlpine: mediator caching behavior, cache contracts/DTOs, Redis-backed cache services, and EF Core-backed cache setting management. Works with IronAlpine.Mediator and IronAlpine.Data.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.1.1 237 10/21/2026
3.0.9 243 10/15/2026
3.0.8 141 10/13/2026
3.0.7 128 10/13/2026
3.0.6 132 10/9/2026
3.0.5 210 10/5/2026
3.0.4 229 10/2/2026
3.0.3 194 9/30/2026
3.0.2 1,033 7/22/2026
3.0.1 159 7/22/2026
3.0.0 370 7/20/2026
2.0.3 171 7/20/2026
2.0.2 196 7/13/2026
2.0.1 197 7/13/2026
2.0.0 191 7/13/2026
2.0.0-preview.2 91 7/13/2026
2.0.0-preview.1 93 7/6/2026
1.0.2 162 7/3/2026
1.0.1 163 7/3/2026
Loading failed

Stable mediator release with request/response, notification publish strategies, streaming, and dependency injection integration.