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
                    
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="Nera.Lib.Database" Version="1.0.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Nera.Lib.Database" Version="1.0.3" />
                    
Directory.Packages.props
<PackageReference Include="Nera.Lib.Database" />
                    
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 Nera.Lib.Database --version 1.0.3
                    
#r "nuget: Nera.Lib.Database, 1.0.3"
                    
#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 Nera.Lib.Database@1.0.3
                    
#: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=Nera.Lib.Database&version=1.0.3
                    
Install as a Cake Addin
#tool nuget:?package=Nera.Lib.Database&version=1.0.3
                    
Install as a Cake Tool

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:

  1. Update Domain Events:

    // Old
    public record UserCreatedEvent(Guid UserId) : ICustomDomainEvent
    
    // New
    public record UserCreatedEvent(Guid UserId) : DomainEventBase
    
  2. Update Aggregates:

    // Old
    public class User : BaseEntity
    
    // New
    public class User : AggregateRoot
    
  3. 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

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Add tests
  5. 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 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. 
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 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.

Version Downloads Last Updated
1.0.3 181 9/25/2025
1.0.2 239 9/15/2025
1.0.1 219 9/9/2025
1.0.0 128 8/3/2025