IronAlpine.Framework.Data
1.0.1
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
<PackageReference Include="IronAlpine.Framework.Data" Version="1.0.1" />
<PackageVersion Include="IronAlpine.Framework.Data" Version="1.0.1" />
<PackageReference Include="IronAlpine.Framework.Data" />
paket add IronAlpine.Framework.Data --version 1.0.1
#r "nuget: IronAlpine.Framework.Data, 1.0.1"
#:package IronAlpine.Framework.Data@1.0.1
#addin nuget:?package=IronAlpine.Framework.Data&version=1.0.1
#tool nuget:?package=IronAlpine.Framework.Data&version=1.0.1
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
- Overview
- Core Concepts
- Installation
- Repository Pattern
- Unit of Work Pattern
- EF Core Integration
- Domain Event Dispatch
- Auditing Behavior
- Transaction Handling
- Specification Pattern
- Redis Caching Integration
- Query Optimization
- Complete API Reference
- Best Practices
- Common Scenarios
- Troubleshooting
- Performance Considerations
- 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:
- Abstraction: Hide database details from business logic
- Testability: Mock repositories in unit tests
- Consistency: Standardized query interface
- 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
- IronAlpine.Data: Repository and Unit of Work abstractions
- IronAlpine.Data.EFCore: EF Core implementation
- 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
- Use AsNoTracking for read-only queries:
var products = await _productRepository.AsNoTracking()
.Select(p => new ProductDto { Id = p.Id, Name = p.Name })
.ToListAsync(ct);
- Limit result sets:
var recent = await _orderRepository.AsQueryable()
.OrderByDescending(o => o.CreatedAt)
.Take(100)
.ToListAsync(ct);
- Use indexes on frequently filtered columns:
builder.HasIndex(u => u.Email).IsUnique();
builder.HasIndex(u => u.CreatedAt);
- 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 | Versions 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. |
-
net10.0
- IronAlpine.Framework.Kernel (>= 1.0.1)
- IronAlpine.Framework.Mediator (>= 1.0.1)
- Microsoft.EntityFrameworkCore (>= 9.0.7)
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.7)
- Microsoft.Extensions.Caching.Memory (>= 9.0.7)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.7)
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.7)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 9.0.7)
- StackExchange.Redis (>= 2.8.37)
-
net9.0
- IronAlpine.Framework.Kernel (>= 1.0.1)
- IronAlpine.Framework.Mediator (>= 1.0.1)
- Microsoft.EntityFrameworkCore (>= 9.0.7)
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.7)
- Microsoft.Extensions.Caching.Memory (>= 9.0.7)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.7)
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.7)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 9.0.7)
- StackExchange.Redis (>= 2.8.37)
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 |
Stable mediator release with request/response, notification publish strategies, streaming, and dependency injection integration.