MinimalCleanArch.DataAccess
0.1.4
dotnet add package MinimalCleanArch.DataAccess --version 0.1.4
NuGet\Install-Package MinimalCleanArch.DataAccess -Version 0.1.4
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="MinimalCleanArch.DataAccess" Version="0.1.4" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MinimalCleanArch.DataAccess" Version="0.1.4" />
<PackageReference Include="MinimalCleanArch.DataAccess" />
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 MinimalCleanArch.DataAccess --version 0.1.4
The NuGet Team does not provide support for this client. Please contact its maintainers for support.
#r "nuget: MinimalCleanArch.DataAccess, 0.1.4"
#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.
#addin nuget:?package=MinimalCleanArch.DataAccess&version=0.1.4
#tool nuget:?package=MinimalCleanArch.DataAccess&version=0.1.4
The NuGet Team does not provide support for this client. Please contact its maintainers for support.
MinimalCleanArch
A comprehensive library for implementing Clean Architecture with Minimal API in .NET 8+.
๐ Features
- Clean Architecture Foundation: Domain entities, repositories, specifications, and unit of work patterns
- Minimal API Extensions: Fluent validation, error handling, and standardized responses
- Security & Encryption: Column-level encryption with Microsoft Data Protection API
- Soft Delete & Auditing: Automatic tracking of creation, modification, and deletion
- Specification Pattern: Encapsulate complex queries in reusable, testable objects
- Result Pattern: Type-safe error handling without exceptions
- Entity Framework Integration: Complete EF Core implementation with best practices
๐ฆ Packages
Package | Description |
---|---|
MinimalCleanArch | Core interfaces and base classes |
MinimalCleanArch.DataAccess | EF Core implementation |
MinimalCleanArch.Extensions | Minimal API extensions and validation |
MinimalCleanArch.Validation | FluentValidation integration |
MinimalCleanArch.Security | Data encryption and security features |
๐ง Quick Start
1. Install Packages
dotnet add package MinimalCleanArch
dotnet add package MinimalCleanArch.DataAccess
dotnet add package MinimalCleanArch.Extensions
dotnet add package MinimalCleanArch.Validation
dotnet add package MinimalCleanArch.Security
2. Define Your Domain Entity
public class Todo : BaseSoftDeleteEntity
{
public string Title { get; private set; }
[Encrypted] // Automatically encrypted in database
public string Description { get; private set; }
public int Priority { get; private set; }
public DateTime? DueDate { get; private set; }
public bool IsCompleted { get; private set; }
public Todo(string title, string description, int priority = 0, DateTime? dueDate = null)
{
if (string.IsNullOrWhiteSpace(title))
throw new DomainException("Title cannot be empty");
if (priority < 0 || priority > 5)
throw new DomainException("Priority must be between 0 and 5");
Title = title;
Description = description;
Priority = priority;
DueDate = dueDate;
}
public void Update(string title, string description, int priority, DateTime? dueDate)
{
if (string.IsNullOrWhiteSpace(title))
throw new DomainException("Title cannot be empty");
Title = title;
Description = description;
Priority = priority;
DueDate = dueDate;
}
public void MarkAsCompleted() => IsCompleted = true;
}
3. Create Your DbContext
public class ApplicationDbContext : DbContextBase
{
public DbSet<Todo> Todos => Set<Todo>();
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Configure entities
modelBuilder.Entity<Todo>(entity =>
{
entity.Property(e => e.Title).HasMaxLength(200).IsRequired();
entity.Property(e => e.Description).HasMaxLength(1000);
entity.HasIndex(e => e.Priority);
});
base.OnModelCreating(modelBuilder);
}
protected override string? GetCurrentUserId()
{
// Return current user ID from your auth system
return "system"; // or get from HttpContext
}
}
4. Configure Services
var builder = WebApplication.CreateBuilder(args);
// Add database with encryption
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
// Add MinimalCleanArch services
builder.Services.AddMinimalCleanArch<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
// Add encryption (choose one approach)
// Option 1: File-based key storage (good for single server)
builder.Services.AddDataProtectionEncryption("./keys", "MyApp");
// Option 2: Azure Key Vault (recommended for production)
// builder.Services.AddDataProtectionEncryptionWithAzureKeyVault(
// "https://myvault.vault.azure.net/", "my-key", "MyApp");
// Add validation
builder.Services.AddValidatorsFromAssemblyContaining<CreateTodoValidator>();
var app = builder.Build();
// Add error handling middleware
app.UseMiddleware<ErrorHandlingMiddleware>();
5. Create API Endpoints
// Create Todo
app.MapPost("/api/todos", async (
CreateTodoRequest request,
IRepository<Todo> repository,
IUnitOfWork unitOfWork) =>
{
var todo = new Todo(request.Title, request.Description, request.Priority, request.DueDate);
await repository.AddAsync(todo);
await unitOfWork.SaveChangesAsync();
return Results.Created($"/api/todos/{todo.Id}", new TodoResponse(todo));
})
.WithValidation<CreateTodoRequest>()
.WithErrorHandling()
.WithStandardResponses<TodoResponse>();
// Get Todo with specification
app.MapGet("/api/todos", async (
int? priority,
bool? isCompleted,
string? searchTerm,
int pageIndex = 1,
int pageSize = 10,
IRepository<Todo> repository) =>
{
var filterSpec = new TodoFilterSpecification(priority, isCompleted, searchTerm);
var paginatedSpec = new TodoPaginatedSpecification(pageSize, pageIndex, filterSpec);
var todos = await repository.GetAsync(paginatedSpec);
var totalCount = await repository.CountAsync(filterSpec.Criteria);
return Results.Ok(new
{
Items = todos.Select(t => new TodoResponse(t)),
Pagination = new
{
CurrentPage = pageIndex,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
}
});
});
// Update Todo
app.MapPut("/api/todos/{id:int}", async (
int id,
UpdateTodoRequest request,
IRepository<Todo> repository,
IUnitOfWork unitOfWork) =>
{
var todo = await repository.GetByIdAsync(id);
if (todo == null)
return Results.NotFound();
todo.Update(request.Title, request.Description, request.Priority, request.DueDate);
await repository.UpdateAsync(todo);
await unitOfWork.SaveChangesAsync();
return Results.Ok(new TodoResponse(todo));
})
.WithValidation<UpdateTodoRequest>()
.WithErrorHandling();
// Soft Delete Todo
app.MapDelete("/api/todos/{id:int}", async (
int id,
IRepository<Todo> repository,
IUnitOfWork unitOfWork) =>
{
var todo = await repository.GetByIdAsync(id);
if (todo == null)
return Results.NotFound();
await repository.DeleteAsync(todo); // Soft delete
await unitOfWork.SaveChangesAsync();
return Results.NoContent();
});
6. Create Specifications for Complex Queries
public class TodoFilterSpecification : BaseSpecification<Todo>
{
public TodoFilterSpecification(
int? priority = null,
bool? isCompleted = null,
string? searchTerm = null,
DateTime? dueBefore = null,
DateTime? dueAfter = null)
{
// Add filters
if (priority.HasValue)
AddCriteria(t => t.Priority == priority.Value);
if (isCompleted.HasValue)
AddCriteria(t => t.IsCompleted == isCompleted.Value);
if (!string.IsNullOrWhiteSpace(searchTerm))
AddCriteria(t => t.Title.Contains(searchTerm) || t.Description.Contains(searchTerm));
if (dueBefore.HasValue)
AddCriteria(t => t.DueDate != null && t.DueDate <= dueBefore.Value);
if (dueAfter.HasValue)
AddCriteria(t => t.DueDate != null && t.DueDate >= dueAfter.Value);
// Default ordering
ApplyOrderByDescending(t => t.Priority);
ApplyThenByDescending(t => t.CreatedAt);
}
}
public class TodoPaginatedSpecification : BaseSpecification<Todo>
{
public TodoPaginatedSpecification(int pageSize, int pageIndex, TodoFilterSpecification filterSpec)
{
if (filterSpec.Criteria != null)
AddCriteria(filterSpec.Criteria);
ApplyOrderByDescending(t => t.Priority);
ApplyThenByDescending(t => t.CreatedAt);
ApplyPaging((pageIndex - 1) * pageSize, pageSize);
}
}
7. Add Validation
public class CreateTodoValidator : AbstractValidator<CreateTodoRequest>
{
public CreateTodoValidator()
{
RuleFor(x => x.Title)
.NotEmpty()
.MaximumLength(200);
RuleFor(x => x.Description)
.MaximumLength(1000);
RuleFor(x => x.Priority)
.InclusiveBetween(0, 5);
RuleFor(x => x.DueDate)
.GreaterThan(DateTime.Now)
.When(x => x.DueDate.HasValue);
}
}
๐ Security & Encryption
Automatic Column Encryption
public class User : BaseAuditableEntity
{
public string Username { get; set; }
[Encrypted] // Automatically encrypted/decrypted
public string Email { get; set; }
[Encrypted]
public string? PhoneNumber { get; set; }
}
Configure Encryption in DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Apply encryption to all [Encrypted] properties
modelBuilder.UseEncryption(_encryptionService);
// Or configure specific properties
modelBuilder.UseEncryptionForProperty<User>(
u => u.Email,
_encryptionService,
allowNull: false);
base.OnModelCreating(modelBuilder);
}
๐ฏ Result Pattern Usage
public class TodoService
{
public async Task<Result<TodoResponse>> CreateTodoAsync(CreateTodoRequest request)
{
try
{
var todo = new Todo(request.Title, request.Description, request.Priority);
await _repository.AddAsync(todo);
await _unitOfWork.SaveChangesAsync();
return Result.Success(new TodoResponse(todo));
}
catch (DomainException ex)
{
return Result.Failure<TodoResponse>(Error.Validation("INVALID_TODO", ex.Message));
}
catch (Exception ex)
{
return Result.Failure<TodoResponse>(Error.FromException(ex));
}
}
}
// Usage in endpoint
app.MapPost("/api/todos", async (CreateTodoRequest request, TodoService service) =>
{
var result = await service.CreateTodoAsync(request);
return result.IsSuccess
? Results.Created($"/api/todos/{result.Value.Id}", result.Value)
: Results.BadRequest(result.Error);
});
๐งช Testing
[Fact]
public async Task Repository_ShouldSoftDelete_WhenEntityDeleted()
{
// Arrange
var todo = new Todo("Test", "Description");
await _repository.AddAsync(todo);
await _unitOfWork.SaveChangesAsync();
// Act
await _repository.DeleteAsync(todo);
await _unitOfWork.SaveChangesAsync();
// Assert
var retrievedTodo = await _repository.GetByIdAsync(todo.Id);
retrievedTodo.Should().BeNull(); // Soft deleted, not returned by default queries
// Verify it still exists with IsDeleted = true
var deletedTodo = await _dbContext.Todos
.IgnoreQueryFilters()
.FirstAsync(t => t.Id == todo.Id);
deletedTodo.IsDeleted.Should().BeTrue();
}
๐ Advanced Features
Transactions
await _unitOfWork.ExecuteInTransactionAsync(async () =>
{
var todo1 = new Todo("Task 1", "Description 1");
var todo2 = new Todo("Task 2", "Description 2");
await _repository.AddRangeAsync(new[] { todo1, todo2 });
await _unitOfWork.SaveChangesAsync();
// Both todos are saved together or rolled back on error
});
Bulk Operations with Extensions
// Add and save in one operation
var todo = await _repository.AddAndSaveAsync(_unitOfWork, newTodo);
// Update and save in one operation
var updatedTodo = await _repository.UpdateAndSaveAsync(_unitOfWork, existingTodo);
Health Checks
builder.Services.AddHealthChecks()
.AddDbContextCheck<ApplicationDbContext>()
.AddCheck<EncryptionHealthCheck>("encryption");
app.MapHealthChecks("/health");
๐ Documentation (coming soon)
๐ค Contributing
Contributions are welcome! Please read our Contributing Guide for details.
๐ License
This project is licensed under the MIT License - see the LICENSE file for details.
MinimalCleanArch - Clean Architecture made simple for .NET developers.
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. |
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
-
net9.0
- Microsoft.AspNetCore.Identity.EntityFrameworkCore (>= 9.0.5)
- Microsoft.EntityFrameworkCore (>= 9.0.5)
- MinimalCleanArch (>= 0.1.4)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.