IntraDotNet.CleanArchitecture.Application
1.0.0
dotnet add package IntraDotNet.CleanArchitecture.Application --version 1.0.0
NuGet\Install-Package IntraDotNet.CleanArchitecture.Application -Version 1.0.0
<PackageReference Include="IntraDotNet.CleanArchitecture.Application" Version="1.0.0" />
<PackageVersion Include="IntraDotNet.CleanArchitecture.Application" Version="1.0.0" />
<PackageReference Include="IntraDotNet.CleanArchitecture.Application" />
paket add IntraDotNet.CleanArchitecture.Application --version 1.0.0
#r "nuget: IntraDotNet.CleanArchitecture.Application, 1.0.0"
#:package IntraDotNet.CleanArchitecture.Application@1.0.0
#addin nuget:?package=IntraDotNet.CleanArchitecture.Application&version=1.0.0
#tool nuget:?package=IntraDotNet.CleanArchitecture.Application&version=1.0.0
IntraDotNet.CleanArchitecture.Application
Table Of Contents
Overview
The IntraDotNet.CleanArchitecture.Application library provides a standardised foundation for the application layer in Clean Architecture projects. This layer orchestrates your use cases, coordinates domain logic, and defines the contracts that infrastructure must implement.
The Application Layer sits between the Domain and Infrastructure layers:
- ✅ Depends on Domain - Uses domain entities and business rules
- ✅ Defines Infrastructure Contracts - Specifies what it needs (repositories, services)
- ✅ Independent of Frameworks - No dependency on databases, web frameworks, or external systems
- ✅ Contains Use Cases - Implements application-specific business logic
What's Included
- Repository Interfaces: Define contracts for data persistence (implemented by Infrastructure layer)
- Unit of Work Pattern: Ensures transactional consistency across operations
- Result Pattern: Type-safe handling of success/failure without exceptions
- Base Services: Reusable service implementations for common CRUD operations
- Validation Services: Services with built-in validation hooks
- User Context Interface: Contract for accessing current user information
Key Concepts
Application Layer Responsibilities
The Application Layer is responsible for:
- 📋 Use Case Implementation - Orchestrating domain logic to fulfill business requirements
- 🔄 Transaction Management - Coordinating multiple repository operations
- ✅ Input Validation - Validating data before domain operations
- 🎭 DTO Mapping - Converting between domain entities and presentation models (in derived services)
- 📝 Defining Contracts - Specifying what infrastructure must provide
What Doesn't Belong Here
The Application Layer should NOT contain:
- ❌ Database implementations (belongs in Infrastructure)
- ❌ HTTP/API concerns (belongs in Presentation/API)
- ❌ Domain business rules (belongs in Domain)
- ❌ External service calls (belongs in Infrastructure)
Project Structure
Common/
├── Persistence/
│ ├── IGuidRepository.cs # GUID-based repository contract
│ ├── IIntRepository.cs # Integer-based repository contract
│ └── IUnitOfWork.cs # Transaction management contract
└── Services/
├── ICurrentUserService.cs # User context contract
├── IGuidDataService.cs # GUID service contract
├── IIntDataService.cs # Integer service contract
├── IGuidValidatableDataService.cs
└── IIntValidatableDataService.cs
Results/
├── Result.cs # Success/failure without value
└── ValueResult.cs # Success/failure with value
Services/
├── GuidDataService.cs # Base GUID service implementation
├── IntDataService.cs # Base Int service implementation
├── GuidValidatableDataService.cs # GUID service with validation
└── IntValidatableDataService.cs # Int service with validation
Core Components
1. Repository Interfaces
Repository interfaces define the contract for data access without specifying implementation details.
IGuidRepository<TEntity>
public interface IGuidRepository<TEntity> where TEntity : class, IGuidIdentifier
{
Task<TEntity?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task<IEnumerable<TEntity>> GetAllAsync(CancellationToken cancellationToken = default);
Task<IEnumerable<TEntity>> FindAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);
Task AddAsync(TEntity entity, CancellationToken cancellationToken = default);
void Update(TEntity entity);
void Delete(TEntity entity);
}
IIntRepository<TEntity>
public interface IIntRepository<TEntity> where TEntity : class, IIntIdentifier
{
Task<TEntity?> GetByIdAsync(int id, CancellationToken cancellationToken = default);
Task<IEnumerable<TEntity>> GetAllAsync(CancellationToken cancellationToken = default);
Task<IEnumerable<TEntity>> FindAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);
Task AddAsync(TEntity entity, CancellationToken cancellationToken = default);
void Update(TEntity entity);
void Delete(TEntity entity);
}
2. Unit of Work Pattern
Ensures multiple operations can be executed as a single transaction.
public interface IUnitOfWork
{
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
Task BeginTransactionAsync(CancellationToken cancellationToken = default);
Task CommitTransactionAsync(CancellationToken cancellationToken = default);
Task RollbackTransactionAsync(CancellationToken cancellationToken = default);
}
3. Result Pattern
Provides type-safe error handling without exceptions.
Result - For operations without return values:
var result = Result.Success();
var result = Result.Failure("Something went wrong");
var result = Result.Failure(new[] { "Error 1", "Error 2" });
ValueResult<T> - For operations with return values:
var result = ValueResult<Product>.Success(product);
var result = ValueResult<Product>.Failure("Product not found");
Checking Results:
if (result.IsSuccess)
{
var value = result.Value;
// Use value
}
else
{
var error = result.Error; // Single error message
var errors = result.AggregateErrors; // Multiple errors
}
4. Base Services
Pre-built service implementations for common CRUD operations.
GuidDataService<TEntity> - For entities with GUID identifiers IntDataService<TEntity> - For entities with integer identifiers
Both provide:
CreateAsync()- Create new entityUpdateAsync()- Update existing entityDeleteAsync()- Delete entity (supports soft delete)DeleteByIdAsync()- Delete by IDGetAllAsync()- Get all entitiesGetByIdAsync()- Get entity by IDFindAsync()- Find entities matching criteria
5. Validatable Services
Services with built-in validation hooks.
GuidValidatableDataService<TEntity> IntValidatableDataService<TEntity>
These extend the base services and add:
ValidateAsync()- Custom validation logic- Automatic validation before Create/Update operations
6. Current User Service
Interface for accessing authenticated user information (implemented in Infrastructure/Presentation layer).
public interface ICurrentUserService
{
string? UserId { get; }
string? UserName { get; }
bool IsAuthenticated { get; }
}
Complete Examples
Example 1: Basic Repository and Service
Step 1: Define Custom Repository Interface
// YourApp.Application/Interfaces/Persistence/IProductRepository.cs
using IntraDotNet.CleanArchitecture.Application.Common.Persistence;
using YourApp.Domain.Entities;
namespace YourApp.Application.Interfaces.Persistence;
public interface IProductRepository : IGuidRepository<Product>
{
Task<Product?> GetByNameAsync(string name, CancellationToken cancellationToken = default);
Task<Product?> GetBySKUAsync(string sku, CancellationToken cancellationToken = default);
Task<IEnumerable<Product>> GetProductsInPriceRangeAsync(decimal min, decimal max, CancellationToken cancellationToken = default);
Task<bool> ExistsBySKUAsync(string sku, Guid? excludeId = null, CancellationToken cancellationToken = default);
}
Step 2: Create Service Interface
// YourApp.Application/Interfaces/Services/IProductService.cs
using IntraDotNet.CleanArchitecture.Application.Common.Services;
using YourApp.Domain.Entities;
namespace YourApp.Application.Interfaces.Services;
public interface IProductService : IGuidDataService<Product>
{
Task<ValueResult<IEnumerable<Product>>> GetProductsInPriceRangeAsync(
decimal minPrice,
decimal maxPrice,
CancellationToken cancellationToken = default);
}
Step 3: Implement Service with Validation
// YourApp.Application/Services/ProductService.cs
using System.Linq.Expressions;
using IntraDotNet.CleanArchitecture.Application.Services;
using IntraDotNet.CleanArchitecture.Application.Results;
using IntraDotNet.CleanArchitecture.Application.Common.Persistence;
using YourApp.Application.Interfaces.Persistence;
using YourApp.Application.Interfaces.Services;
using YourApp.Domain.Entities;
namespace YourApp.Application.Services;
public class ProductService : GuidValidatableDataService<Product>, IProductService
{
private readonly IProductRepository _repository;
private readonly IUnitOfWork _unitOfWork;
public ProductService(
IProductRepository repository,
IUnitOfWork unitOfWork)
{
_repository = repository;
_unitOfWork = unitOfWork;
}
#region Validation
public override async Task<ValueResult<bool>> ValidateAsync(
Product entity,
CancellationToken cancellationToken = default)
{
var errors = new List<string>();
// Required field validation
if (string.IsNullOrWhiteSpace(entity.Name))
errors.Add("Product name is required");
if (string.IsNullOrWhiteSpace(entity.SKU))
errors.Add("Product SKU is required");
// Business rule validation
if (entity.Price <= 0)
errors.Add("Product price must be greater than zero");
if (entity.StockQuantity < 0)
errors.Add("Stock quantity cannot be negative");
// Duplicate check
if (!string.IsNullOrWhiteSpace(entity.SKU))
{
var exists = await _repository.ExistsBySKUAsync(entity.SKU, entity.Id, cancellationToken);
if (exists)
errors.Add($"A product with SKU '{entity.SKU}' already exists");
}
return errors.Any()
? ValueResult<bool>.Failure(errors)
: ValueResult<bool>.Success(true);
}
#endregion
#region CRUD Operations
protected override async Task<ValueResult<Product>> CreateInternalAsync(
Product entity,
CancellationToken cancellationToken = default)
{
await _repository.AddAsync(entity, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return ValueResult<Product>.Success(entity);
}
protected override async Task<ValueResult<Product>> UpdateInternalAsync(
Product entity,
CancellationToken cancellationToken = default)
{
_repository.Update(entity);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return ValueResult<Product>.Success(entity);
}
public override async Task<Result> DeleteAsync(
Product entity,
CancellationToken cancellationToken = default)
{
_repository.Delete(entity);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return Result.Success();
}
public override async Task<Result> DeleteByIdAsync(
Guid id,
CancellationToken cancellationToken = default)
{
var product = await _repository.GetByIdAsync(id, cancellationToken);
if (product == null)
return Result.Failure($"Product with ID {id} not found");
return await DeleteAsync(product, cancellationToken);
}
public override async Task<ValueResult<Product>> GetByIdAsync(
Guid id,
CancellationToken cancellationToken = default)
{
var product = await _repository.GetByIdAsync(id, cancellationToken);
return product != null
? ValueResult<Product>.Success(product)
: ValueResult<Product>.Failure($"Product with ID {id} not found");
}
public override async Task<ValueResult<IEnumerable<Product>>> GetAllAsync(
CancellationToken cancellationToken = default)
{
var products = await _repository.GetAllAsync(cancellationToken);
return ValueResult<IEnumerable<Product>>.Success(products);
}
public override async Task<ValueResult<IEnumerable<Product>>> FindAsync(
Expression<Func<Product, bool>> predicate,
CancellationToken cancellationToken = default)
{
var products = await _repository.FindAsync(predicate, cancellationToken);
return ValueResult<IEnumerable<Product>>.Success(products);
}
#endregion
#region Custom Methods
public async Task<ValueResult<IEnumerable<Product>>> GetProductsInPriceRangeAsync(
decimal minPrice,
decimal maxPrice,
CancellationToken cancellationToken = default)
{
if (minPrice < 0)
return ValueResult<IEnumerable<Product>>.Failure("Minimum price cannot be negative");
if (maxPrice < minPrice)
return ValueResult<IEnumerable<Product>>.Failure("Maximum price must be greater than minimum price");
var products = await _repository.GetProductsInPriceRangeAsync(minPrice, maxPrice, cancellationToken);
return ValueResult<IEnumerable<Product>>.Success(products);
}
#endregion
}
Example 2: Multi-Entity Transaction
// YourApp.Application/Services/OrderService.cs
using IntraDotNet.CleanArchitecture.Application.Services;
using IntraDotNet.CleanArchitecture.Application.Results;
using YourApp.Application.Interfaces.Persistence;
using YourApp.Domain.Entities;
namespace YourApp.Application.Services;
public class OrderService : GuidValidatableDataService<Order>
{
private readonly IOrderRepository _orderRepository;
private readonly IProductRepository _productRepository;
private readonly IUnitOfWork _unitOfWork;
public OrderService(
IOrderRepository orderRepository,
IProductRepository productRepository,
IUnitOfWork unitOfWork)
{
_orderRepository = orderRepository;
_productRepository = productRepository;
_unitOfWork = unitOfWork;
}
public async Task<ValueResult<Order>> CreateOrderWithStockUpdateAsync(
Order order,
CancellationToken cancellationToken = default)
{
// Validate the order
var validationResult = await ValidateAsync(order, cancellationToken);
if (validationResult.IsFailure)
return ValueResult<Order>.Failure(validationResult.AggregateErrors!);
// Start transaction
await _unitOfWork.BeginTransactionAsync(cancellationToken);
try
{
// 1. Create the order
await _orderRepository.AddAsync(order, cancellationToken);
// 2. Update stock for each product
foreach (var item in order.Items)
{
var product = await _productRepository.GetByIdAsync(item.ProductId, cancellationToken);
if (product == null)
{
await _unitOfWork.RollbackTransactionAsync(cancellationToken);
return ValueResult<Order>.Failure($"Product with ID {item.ProductId} not found");
}
if (product.StockQuantity < item.Quantity)
{
await _unitOfWork.RollbackTransactionAsync(cancellationToken);
return ValueResult<Order>.Failure($"Insufficient stock for product {product.Name}");
}
product.StockQuantity -= item.Quantity;
_productRepository.Update(product);
}
// 3. Save all changes
await _unitOfWork.SaveChangesAsync(cancellationToken);
await _unitOfWork.CommitTransactionAsync(cancellationToken);
return ValueResult<Order>.Success(order);
}
catch (Exception ex)
{
await _unitOfWork.RollbackTransactionAsync(cancellationToken);
return ValueResult<Order>.Failure($"Failed to create order: {ex.Message}");
}
}
public override async Task<ValueResult<bool>> ValidateAsync(
Order entity,
CancellationToken cancellationToken = default)
{
var errors = new List<string>();
if (string.IsNullOrWhiteSpace(entity.OrderNumber))
errors.Add("Order number is required");
if (!entity.Items.Any())
errors.Add("Order must contain at least one item");
if (entity.TotalAmount <= 0)
errors.Add("Order total must be greater than zero");
return errors.Any()
? ValueResult<bool>.Failure(errors)
: ValueResult<bool>.Success(true);
}
// ... implement required abstract methods ...
}
Example 3: Service Without Validation (Simple CRUD)
// YourApp.Application/Services/CategoryService.cs
using System.Linq.Expressions;
using IntraDotNet.CleanArchitecture.Application.Services;
using IntraDotNet.CleanArchitecture.Application.Results;
using YourApp.Application.Interfaces.Persistence;
using YourApp.Domain.Entities;
namespace YourApp.Application.Services;
/// <summary>
/// Simple service for Category entities without complex validation.
/// Uses IntDataService for integer-based identifiers.
/// </summary>
public class CategoryService : IntDataService<Category>
{
private readonly ICategoryRepository _repository;
private readonly IUnitOfWork _unitOfWork;
public CategoryService(
ICategoryRepository repository,
IUnitOfWork unitOfWork)
{
_repository = repository;
_unitOfWork = unitOfWork;
}
public override async Task<ValueResult<Category>> CreateAsync(
Category entity,
CancellationToken cancellationToken = default)
{
// Simple validation
if (string.IsNullOrWhiteSpace(entity.Name))
return ValueResult<Category>.Failure("Category name is required");
await _repository.AddAsync(entity, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return ValueResult<Category>.Success(entity);
}
public override async Task<ValueResult<Category>> UpdateAsync(
Category entity,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(entity.Name))
return ValueResult<Category>.Failure("Category name is required");
_repository.Update(entity);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return ValueResult<Category>.Success(entity);
}
public override async Task<Result> DeleteAsync(
Category entity,
CancellationToken cancellationToken = default)
{
_repository.Delete(entity);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return Result.Success();
}
public override async Task<Result> DeleteByIdAsync(
int id,
CancellationToken cancellationToken = default)
{
var category = await _repository.GetByIdAsync(id, cancellationToken);
if (category == null)
return Result.Failure($"Category with ID {id} not found");
return await DeleteAsync(category, cancellationToken);
}
public override async Task<ValueResult<Category>> GetByIdAsync(
int id,
CancellationToken cancellationToken = default)
{
var category = await _repository.GetByIdAsync(id, cancellationToken);
return category != null
? ValueResult<Category>.Success(category)
: ValueResult<Category>.Failure($"Category with ID {id} not found");
}
public override async Task<ValueResult<IEnumerable<Category>>> GetAllAsync(
CancellationToken cancellationToken = default)
{
var categories = await _repository.GetAllAsync(cancellationToken);
return ValueResult<IEnumerable<Category>>.Success(categories);
}
public override async Task<ValueResult<IEnumerable<Category>>> FindAsync(
Expression<Func<Category, bool>> predicate,
CancellationToken cancellationToken = default)
{
var categories = await _repository.FindAsync(predicate, cancellationToken);
return ValueResult<IEnumerable<Category>>.Success(categories);
}
}
Best Practices
✅ Do's
Use Result Pattern for All Public Methods
// ✅ Good public async Task<ValueResult<Product>> GetProductAsync(Guid id) { var product = await _repository.GetByIdAsync(id); return product != null ? ValueResult<Product>.Success(product) : ValueResult<Product>.Failure("Product not found"); }Validate in Services, Not Repositories
// ✅ Good - Validation in service public async Task<ValueResult<Product>> CreateAsync(Product product) { var validationResult = await ValidateAsync(product); if (validationResult.IsFailure) return ValueResult<Product>.Failure(validationResult.AggregateErrors!); await _repository.AddAsync(product); return ValueResult<Product>.Success(product); }Use Unit of Work for Transactions
// ✅ Good await _unitOfWork.BeginTransactionAsync(); try { await _repository1.AddAsync(entity1); await _repository2.UpdateAsync(entity2); await _unitOfWork.SaveChangesAsync(); await _unitOfWork.CommitTransactionAsync(); } catch { await _unitOfWork.RollbackTransactionAsync(); throw; }Keep Services Focused
// ✅ Good - Single responsibility public class ProductService : GuidValidatableDataService<Product> { // Handles only Product-related operations } public class OrderService : GuidValidatableDataService<Order> { // Handles only Order-related operations }Define Repository Interfaces in Application Layer
// ✅ Good - Interface in Application, Implementation in Infrastructure // YourApp.Application/Interfaces/Persistence/IProductRepository.cs public interface IProductRepository : IGuidRepository<Product> { Task<Product?> GetBySKUAsync(string sku); }
❌ Don'ts
Don't Put Infrastructure Code in Application
// ❌ Bad - DbContext in Application layer public class ProductService { private readonly DbContext _context; // NO! } // ✅ Good - Use repository interface public class ProductService { private readonly IProductRepository _repository; // YES! }Don't Swallow Exceptions
// ❌ Bad try { await _repository.AddAsync(product); } catch { return null; // Lost the error! } // ✅ Good try { await _repository.AddAsync(product); return ValueResult<Product>.Success(product); } catch (Exception ex) { return ValueResult<Product>.Failure($"Failed to create product: {ex.Message}"); }Don't Return Domain Entities Directly from API
// ❌ Bad - Exposing domain entity to API [HttpGet] public async Task<Product> GetProduct(Guid id) { return await _service.GetByIdAsync(id); } // ✅ Good - Map to DTO in controller [HttpGet] public async Task<IActionResult> GetProduct(Guid id) { var result = await _service.GetByIdAsync(id); if (result.IsFailure) return NotFound(result.Error); var dto = MapToDto(result.Value); return Ok(dto); }Don't Mix Query and Command Logic
// ❌ Bad - Doing too much in one method public async Task<Product> GetAndUpdateProductAsync(Guid id, decimal newPrice) { var product = await _repository.GetByIdAsync(id); product.Price = newPrice; await _repository.UpdateAsync(product); return product; } // ✅ Good - Separate concerns public async Task<ValueResult<Product>> GetProductAsync(Guid id) { /* ... */ } public async Task<ValueResult<Product>> UpdateProductPriceAsync(Guid id, decimal newPrice) { /* ... */ }
When to Use Which Service
| Scenario | Use |
|---|---|
| Simple CRUD with minimal validation | GuidDataService or IntDataService |
| Complex business rules and validation | GuidValidatableDataService or IntValidatableDataService |
| Need different DTOs for different operations | Create custom service methods in derived class |
| Multi-entity transactions | Inject multiple repositories and use IUnitOfWork |
| Read-only operations | Consider creating separate query services/handlers |
Integration with Other Layers
With Domain Layer
// Application service orchestrates domain entities
public async Task<ValueResult<Order>> PlaceOrderAsync(Order order)
{
// Use domain methods
order.CalculateTotal();
order.Validate(); // Domain validation
// Persist through repository
await _repository.AddAsync(order);
await _unitOfWork.SaveChangesAsync();
return ValueResult<Order>.Success(order);
}
With Infrastructure Layer
// Application defines the contract
public interface IProductRepository : IGuidRepository<Product>
{
Task<Product?> GetBySKUAsync(string sku);
}
// Infrastructure implements it
public class ProductRepository : GuidRepository<Product, AppDbContext>, IProductRepository
{
public async Task<Product?> GetBySKUAsync(string sku)
{
return await Context.Products.FirstOrDefaultAsync(p => p.SKU == sku);
}
}
With Presentation Layer
// Controller uses service through interface
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
[HttpPost]
public async Task<IActionResult> Create(CreateProductDto dto)
{
var product = MapToEntity(dto);
var result = await _productService.CreateAsync(product);
if (result.IsFailure)
return BadRequest(result.Error);
return CreatedAtAction(nameof(Get), new { id = result.Value.Id }, MapToDto(result.Value));
}
}
See Also
- Domain Layer - Domain entities and business logic
- Infrastructure.EFCore - Entity Framework Core implementation
- Clean Architecture Principles
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. 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. |
-
net8.0
- IntraDotNet.CleanArchitecture.Domain (>= 1.0.0)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on IntraDotNet.CleanArchitecture.Application:
| Package | Downloads |
|---|---|
|
KeyZee
A minimal self-hostable encrypted key value pair sdk that can be used with any modern relational database written in .NET Core |
|
|
IntraDotNet.CleanArchitecture.Infrastructure.EFCore
Entity Framework Core infrastructure implementation for Clean Architecture. Provides AuditableDbContext with automatic audit tracking, repository implementations, Unit of Work pattern, and soft delete support. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 175 | 12/31/2025 |
Initial release of IntraDotNet.CleanArchitecture.Application
- IGuidRepository and IIntRepository interfaces
- IUnitOfWork interface for transaction management
- Result and ValueResult types for error handling
- GuidDataService and IntDataService base implementations
- GuidValidatableDataService and IntValidatableDataService with validation hooks
- ICurrentUserService interface for user context