Nera.Lib.Database
1.0.3
dotnet add package Nera.Lib.Database --version 1.0.3
NuGet\Install-Package Nera.Lib.Database -Version 1.0.3
<PackageReference Include="Nera.Lib.Database" Version="1.0.3" />
<PackageVersion Include="Nera.Lib.Database" Version="1.0.3" />
<PackageReference Include="Nera.Lib.Database" />
paket add Nera.Lib.Database --version 1.0.3
#r "nuget: Nera.Lib.Database, 1.0.3"
#:package Nera.Lib.Database@1.0.3
#addin nuget:?package=Nera.Lib.Database&version=1.0.3
#tool nuget:?package=Nera.Lib.Database&version=1.0.3
Nera.Lib.Database
๐ Overview
Nera.Lib.Database
is a comprehensive library that provides infrastructure for database access layer with Entity Framework Core, Repository pattern, Specification pattern, and advanced querying capabilities for Nera applications.
๐๏ธ Architecture
Core Components
Nera.Lib.Database/
โโโ Abstractions/ # Interfaces and abstractions
โโโ Aggregates/ # Aggregate root implementations
โโโ Configurations/ # EF Core entity configurations
โโโ DomainEvent/ # Domain event infrastructure
โโโ Entities/ # Base entity classes
โโโ Events/ # Event publishing infrastructure
โโโ Filters/ # Global query filters
โโโ Interceptor/ # EF Core interceptors
โโโ Outbox/ # Outbox pattern implementation
โโโ Pagination/ # Pagination utilities
โโโ Persistence/ # EF Core configurations
โโโ Repositories/ # Repository implementations
โโโ UoW/ # Unit of Work pattern
โโโ UnitOfWork/ # Unit of Work implementations
๐ Quick Start
1. Install Package
dotnet add package Nera.Lib.Database
2. Configure Services
// Program.cs
using Nera.Lib.Database;
var builder = WebApplication.CreateBuilder(args);
// Add database services
builder.Services.AddDatabaseServices<YourDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("Default"));
options.UseDomainEvents(); // Enable domain event publishing
options.UseOutbox(); // Enable outbox pattern
});
3. Create Your DbContext
public class YourDbContext : DbContext
{
public YourDbContext(DbContextOptions<YourDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Apply configurations
modelBuilder.ApplyConfigurationsFromAssembly(typeof(YourDbContext).Assembly);
// Apply global filters
modelBuilder.ApplyGlobalFilters<IMultiTenantEntity>(e => e.OrgId == CurrentTenant.Id);
}
}
๐ Domain Events Architecture
Overview
Domain events represent something that happened in the domain that domain experts care about. This library provides a complete infrastructure for domain event management with clean separation between domain events and integration events.
Key Components
1. Domain Event Interface
// Nera.Lib.Domain.Events.IDomainEvent
public interface IDomainEvent
{
Guid Id { get; }
DateTime OccurredOn { get; }
}
2. Domain Event Base Class
// Nera.Lib.Domain.Events.DomainEventBase
public abstract class DomainEventBase : IntegrationMessageBase
{
protected DomainEventBase()
{
Id = Guid.NewGuid();
OccurredOn = DateTime.UtcNow;
}
public Guid Id { get; }
public DateTime OccurredOn { get; }
}
3. Creating Domain Events
public record UserCreatedEvent(Guid UserId, string Email, Guid OrgId) : DomainEventBase
{
public UserCreatedEvent(Guid userId, string email, Guid orgId) : base()
{
UserId = userId;
Email = email;
OrgId = orgId;
}
}
Aggregate Roots with Domain Events
1. Base Aggregate Root
public abstract class AggregateRoot<TKey> : BaseEntity<TKey>, IAggregateRoot<TKey>
where TKey : notnull
{
private readonly List<IDomainEvent> _domainEvents = new();
public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
protected void AddDomainEvent(IDomainEvent domainEvent)
{
_domainEvents.Add(domainEvent);
}
public void ClearDomainEvents()
{
_domainEvents.Clear();
}
}
2. Using in Your Aggregates
public class User : AggregateRoot
{
public string Email { get; private set; }
public Guid OrgId { get; private set; }
private User() { } // For EF Core
public static User Create(string email, Guid orgId)
{
var user = new User
{
Id = Guid.NewGuid(),
Email = email,
OrgId = orgId
};
user.AddDomainEvent(new UserCreatedEvent(user.Id, email, orgId));
return user;
}
public void UpdateEmail(string newEmail)
{
Email = newEmail;
AddDomainEvent(new UserEmailUpdatedEvent(Id, newEmail, OrgId));
}
}
Domain Event Publishing
1. Event Publisher
public interface IEventPublisher
{
Task PublishAsync<T>(T @event) where T : IntegrationMessageBase;
Task PublishAsync(IDomainEvent @event);
}
public class EventPublisher : IEventPublisher
{
private readonly IMediator _mediator;
public EventPublisher(IMediator mediator)
{
_mediator = mediator;
}
public async Task PublishAsync<T>(T @event) where T : IntegrationMessageBase
{
await _mediator.Publish(@event);
}
public async Task PublishAsync(IDomainEvent @event)
{
await _mediator.Publish(@event);
}
}
2. Domain Event Handlers
public class UserCreatedEventHandler : INotificationHandler<UserCreatedEvent>
{
private readonly ILogger<UserCreatedEventHandler> _logger;
public UserCreatedEventHandler(ILogger<UserCreatedEventHandler> logger)
{
_logger = logger;
}
public async Task Handle(UserCreatedEvent notification, CancellationToken cancellationToken)
{
_logger.LogInformation("User created: {UserId}", notification.UserId);
// Handle domain event logic
await Task.CompletedTask;
}
}
Integration Events
For cross-service communication, use IIntegrationEvent
from Nera.Lib.Messaging.Abstractions
:
public record UserCreatedIntegrationEvent(Guid UserId, string Email) : IIntegrationEvent
{
public Guid Id { get; } = Guid.NewGuid();
public DateTimeOffset OccurredAt { get; } = DateTimeOffset.UtcNow;
}
Outbox Pattern
1. Outbox Message
public sealed class OutboxMessage : BaseEntity<Guid>, IAggregateRoot
{
public required string Type { get; init; }
public required string Content { get; init; }
public OutboxStatus Status { get; private set; } = OutboxStatus.Pending;
public DateTimeOffset PublishedOn { get; init; }
public DateTimeOffset? ProcessedOn { get; private set; }
public string? Message { get; private set; }
public byte RetryAttempt { get; private set; }
public static OutboxMessage Create(string assemblyQualifiedName, IDomainEvent @event)
{
return new OutboxMessage
{
Status = OutboxStatus.Pending,
Type = assemblyQualifiedName,
Content = JsonSerializer.Serialize(@event),
PublishedOn = DateTime.UtcNow
};
}
public void Processing() => Status = OutboxStatus.Processing;
public void Processed() => Status = OutboxStatus.Success;
public void Fail(string? message) => Status = OutboxStatus.Failure;
}
2. Outbox Repository
public interface IOutboxMessageRepository
{
Task<IEnumerable<OutboxMessage>> GetPendingMessagesAsync(CancellationToken cancellationToken = default);
Task UpdateStatusAsync(Guid id, OutboxStatus status, CancellationToken cancellationToken = default);
}
๐ง Repository Pattern
1. Base Repository Interface
public interface IRepository<TEntity, TKey> where TEntity : class, IEntity<TKey>
{
Task<TEntity?> GetByIdAsync(TKey id, CancellationToken cancellationToken = default);
Task<IEnumerable<TEntity>> GetAllAsync(CancellationToken cancellationToken = default);
Task<TEntity> AddAsync(TEntity entity, CancellationToken cancellationToken = default);
Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default);
Task DeleteAsync(TKey id, CancellationToken cancellationToken = default);
}
2. Base Repository Implementation
public abstract class BaseRepository<TEntity, TKey> : IRepository<TEntity, TKey>
where TEntity : class, IEntity<TKey>
{
protected readonly DbContext _context;
protected readonly DbSet<TEntity> _dbSet;
protected BaseRepository(DbContext context)
{
_context = context;
_dbSet = context.Set<TEntity>();
}
public virtual async Task<TEntity?> GetByIdAsync(TKey id, CancellationToken cancellationToken = default)
{
return await _dbSet.FindAsync(new object[] { id }, cancellationToken);
}
public virtual async Task<IEnumerable<TEntity>> GetAllAsync(CancellationToken cancellationToken = default)
{
return await _dbSet.ToListAsync(cancellationToken);
}
public virtual async Task<TEntity> AddAsync(TEntity entity, CancellationToken cancellationToken = default)
{
var result = await _dbSet.AddAsync(entity, cancellationToken);
return result.Entity;
}
public virtual async Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default)
{
_dbSet.Update(entity);
await Task.CompletedTask;
}
public virtual async Task DeleteAsync(TKey id, CancellationToken cancellationToken = default)
{
var entity = await GetByIdAsync(id, cancellationToken);
if (entity != null)
{
_dbSet.Remove(entity);
}
}
}
3. Custom Repository
public interface IUserRepository : IRepository<User, Guid>
{
Task<User?> GetByEmailAsync(string email, CancellationToken cancellationToken = default);
Task<bool> ExistsByEmailAsync(string email, CancellationToken cancellationToken = default);
}
public class UserRepository : BaseRepository<User, Guid>, IUserRepository
{
public UserRepository(YourDbContext context) : base(context)
{
}
public async Task<User?> GetByEmailAsync(string email, CancellationToken cancellationToken = default)
{
return await _dbSet.FirstOrDefaultAsync(u => u.Email == email, cancellationToken);
}
public async Task<bool> ExistsByEmailAsync(string email, CancellationToken cancellationToken = default)
{
return await _dbSet.AnyAsync(u => u.Email == email, cancellationToken);
}
}
๐ Specification Pattern
1. Base Specification
public abstract class Specification<T>
{
public abstract Expression<Func<T, bool>> ToExpression();
public bool IsSatisfiedBy(T entity)
{
return ToExpression().Compile()(entity);
}
public Specification<T> And(Specification<T> specification)
{
return new AndSpecification<T>(this, specification);
}
public Specification<T> Or(Specification<T> specification)
{
return new OrSpecification<T>(this, specification);
}
public Specification<T> Not()
{
return new NotSpecification<T>(this);
}
}
2. Using Specifications
public class UserByEmailSpecification : Specification<User>
{
private readonly string _email;
public UserByEmailSpecification(string email)
{
_email = email;
}
public override Expression<Func<User, bool>> ToExpression()
{
return user => user.Email == _email;
}
}
public class ActiveUserSpecification : Specification<User>
{
public override Expression<Func<User, bool>> ToExpression()
{
return user => user.IsActive;
}
}
// Usage
var spec = new UserByEmailSpecification("user@example.com")
.And(new ActiveUserSpecification());
var user = await userRepository.GetAsync(spec);
๐ Unit of Work Pattern
1. Unit of Work Interface
public interface IUnitOfWork : IDisposable
{
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
Task BeginTransactionAsync(CancellationToken cancellationToken = default);
Task CommitTransactionAsync(CancellationToken cancellationToken = default);
Task RollbackTransactionAsync(CancellationToken cancellationToken = default);
}
2. Unit of Work Implementation
public class UnitOfWork : IUnitOfWork
{
private readonly DbContext _context;
private IDbContextTransaction? _transaction;
public UnitOfWork(DbContext context)
{
_context = context;
}
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
return await _context.SaveChangesAsync(cancellationToken);
}
public async Task BeginTransactionAsync(CancellationToken cancellationToken = default)
{
_transaction = await _context.Database.BeginTransactionAsync(cancellationToken);
}
public async Task CommitTransactionAsync(CancellationToken cancellationToken = default)
{
if (_transaction != null)
{
await _transaction.CommitAsync(cancellationToken);
}
}
public async Task RollbackTransactionAsync(CancellationToken cancellationToken = default)
{
if (_transaction != null)
{
await _transaction.RollbackAsync(cancellationToken);
}
}
public void Dispose()
{
_transaction?.Dispose();
}
}
๐ Pagination
1. Pagination Request
public class PaginationRequest
{
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 10;
public string? SortBy { get; set; }
public string? SortDirection { get; set; }
public string? SearchTerm { get; set; }
}
2. Pagination Result
public class PaginatedResult<T>
{
public IEnumerable<T> Items { get; set; } = Enumerable.Empty<T>();
public int TotalCount { get; set; }
public int Page { get; set; }
public int PageSize { get; set; }
public int TotalPages { get; set; }
public bool HasNextPage { get; set; }
public bool HasPreviousPage { get; set; }
}
3. Using Pagination
public async Task<PaginatedResult<User>> GetUsersAsync(PaginationRequest request, CancellationToken cancellationToken = default)
{
var query = _dbSet.AsQueryable();
// Apply search
if (!string.IsNullOrEmpty(request.SearchTerm))
{
query = query.Where(u => u.Email.Contains(request.SearchTerm));
}
// Apply sorting
if (!string.IsNullOrEmpty(request.SortBy))
{
query = request.SortDirection?.ToLower() == "desc"
? query.OrderByDescending(u => EF.Property<object>(u, request.SortBy))
: query.OrderBy(u => EF.Property<object>(u, request.SortBy));
}
var totalCount = await query.CountAsync(cancellationToken);
var items = await query
.Skip((request.Page - 1) * request.PageSize)
.Take(request.PageSize)
.ToListAsync(cancellationToken);
return new PaginatedResult<User>
{
Items = items,
TotalCount = totalCount,
Page = request.Page,
PageSize = request.PageSize,
TotalPages = (int)Math.Ceiling((double)totalCount / request.PageSize),
HasNextPage = request.Page < (int)Math.Ceiling((double)totalCount / request.PageSize),
HasPreviousPage = request.Page > 1
};
}
๐ Multi-Tenancy
1. Multi-Tenant Entity
public interface IMultiTenantEntity
{
Guid OrgId { get; set; }
}
public abstract class MultiTenantEntity<TKey> : BaseEntity<TKey>, IMultiTenantEntity
where TKey : notnull
{
public Guid OrgId { get; set; }
}
2. Global Filters
public static class ModelBuilderExtensions
{
public static void ApplyGlobalFilters<TInterface>(this ModelBuilder modelBuilder, Expression<Func<TInterface, bool>> expression)
{
var entities = modelBuilder.Model
.GetEntityTypes()
.Where(e => e.ClrType.GetInterface(typeof(TInterface).Name) != null)
.Select(e => e.ClrType);
foreach (var entity in entities)
{
var newParam = Expression.Parameter(entity);
var newbody = ReplacingExpressionVisitor.Replace(
expression.Parameters.Single(), newParam, expression.Body);
modelBuilder.Entity(entity).HasQueryFilter(Expression.Lambda(newbody, newParam));
}
}
}
๐งช Testing
1. In-Memory Database
[TestFixture]
public class UserRepositoryTests
{
private DbContext _context;
private IUserRepository _repository;
[SetUp]
public void Setup()
{
var options = new DbContextOptionsBuilder<YourDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
_context = new YourDbContext(options);
_repository = new UserRepository(_context);
}
[Test]
public async Task GetByEmailAsync_ShouldReturnUser_WhenUserExists()
{
// Arrange
var user = User.Create("test@example.com", Guid.NewGuid());
await _repository.AddAsync(user);
await _context.SaveChangesAsync();
// Act
var result = await _repository.GetByEmailAsync("test@example.com");
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Email, Is.EqualTo("test@example.com"));
}
}
๐ Best Practices
1. Domain Events
- โ
Use
IDomainEvent
for events within the same bounded context - โ
Use
IIntegrationEvent
for cross-service communication - โ Keep events simple and focused on domain concerns
- โ
Use past tense for event names (e.g.,
UserCreatedEvent
) - โ Include relevant context and IDs in events
2. Repositories
- โ Keep repositories focused on data access
- โ Use specifications for complex queries
- โ Implement caching strategies where appropriate
- โ Use async/await consistently
3. Unit of Work
- โ Use transactions for operations that modify multiple entities
- โ Handle transaction rollback in case of errors
- โ Dispose of transactions properly
4. Performance
- โ
Use
AsNoTracking()
for read-only queries - โ Implement pagination for large datasets
- โ Use appropriate indexes on database
- โ Consider using compiled queries for frequently used queries
๐ง Configuration
1. Service Registration
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddDatabaseServices<TContext>(
this IServiceCollection services,
Action<DbContextOptionsBuilder> configureOptions) where TContext : DbContext
{
services.AddDbContext<TContext>(configureOptions);
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddScoped<IEventPublisher, EventPublisher>();
// Register repositories
services.AddScoped<IUserRepository, UserRepository>();
return services;
}
}
2. Domain Event Publishing
public static class ServiceCollectionExtensions
{
public static IServiceCollection UseDomainEvents(this IServiceCollection services)
{
services.AddScoped<IDomainEventDispatcher, DomainEventDispatcher>();
services.AddScoped<IDomainEventPublisher, DomainEventPublisher>();
return services;
}
}
๐ Migration Guide
From Old Architecture
If you're migrating from an older version or custom implementation:
Update Domain Events:
// Old public record UserCreatedEvent(Guid UserId) : ICustomDomainEvent // New public record UserCreatedEvent(Guid UserId) : DomainEventBase
Update Aggregates:
// Old public class User : BaseEntity // New public class User : AggregateRoot
Update Repositories:
// Old public interface IUserRepository // New public interface IUserRepository : IRepository<User, Guid>
๐ License
This project is licensed under the MIT License - see the LICENSE file for details.
๐ค Contributing
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests
- Submit a pull request
๐ Support
For support and questions:
- Create an issue in the repository
- Contact the development team
- Check the documentation
Nera.Lib.Database - Empowering your data access layer with clean architecture and domain-driven design principles.
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 was computed. 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. |
-
net9.0
- MediatR (>= 13.0.0)
- Microsoft.EntityFrameworkCore (>= 9.0.8)
- Nera.Lib.Messaging.Abstractions (>= 1.0.0)
NuGet packages (3)
Showing the top 3 NuGet packages that depend on Nera.Lib.Database:
Package | Downloads |
---|---|
Nera.Lib.Grpc.Common
Common gRPC patterns and helpers for Nera microservices |
|
Nera.Lib.Elasticsearch
Elasticsearch integration library for Nera framework with advanced pagination and search capabilities |
|
Nera.Lib.Test
Testing utilities and helpers for Nera applications |
GitHub repositories
This package is not used by any popular GitHub repositories.