Odex.AspNetCore.Clarc.Application
0.1.0
See the version list below for details.
dotnet add package Odex.AspNetCore.Clarc.Application --version 0.1.0
NuGet\Install-Package Odex.AspNetCore.Clarc.Application -Version 0.1.0
<PackageReference Include="Odex.AspNetCore.Clarc.Application" Version="0.1.0" />
<PackageVersion Include="Odex.AspNetCore.Clarc.Application" Version="0.1.0" />
<PackageReference Include="Odex.AspNetCore.Clarc.Application" />
paket add Odex.AspNetCore.Clarc.Application --version 0.1.0
#r "nuget: Odex.AspNetCore.Clarc.Application, 0.1.0"
#:package Odex.AspNetCore.Clarc.Application@0.1.0
#addin nuget:?package=Odex.AspNetCore.Clarc.Application&version=0.1.0
#tool nuget:?package=Odex.AspNetCore.Clarc.Application&version=0.1.0
βοΈ Odex.AspNetCore.Clarc.Application
Application layer for CQRSβbased ASP.NET Core applications
Provides MediatR pipelines, CQRS abstractions, FluentValidation integration, and typed application exceptions.
π¦ Overview
Odex.AspNetCore.Clarc.Application is the application layer component of the Clarc framework. It bridges the domain and
infrastructure layers by implementing CQRS (Command Query Responsibility Segregation) patterns using MediatR and
FluentValidation. It provides:
- CQRS Abstractions β Base records for commands, queries, and responses.
- Validation Pipeline β Automatic validation of requests using FluentValidation.
- Paged Requests & Responses β Reusable pagination and sorting models.
- Application Exceptions β Typed exceptions for validation, duplicates, service failures, authorization, etc.
- DI Extensions β Oneβline registration of MediatR, validators, and pipeline behaviors.
π Get Started
Prerequisites
- .NET 9.0 SDK or later
- An ASP.NET Core project
- (Optional) Odex.AspNetCore.Clarc.Domain
Installation
dotnet add package Odex.AspNetCore.Clarc.Application
Or using the Package Manager Console:
Install-Package Odex.AspNetCore.Clarc.Application
Basic Setup
1. Register the Application Layer in Program.cs
using Odex.AspNetCore.Clarc.Application;
var builder = WebApplication.CreateBuilder(args);
// Register MediatR, validators, and validation pipeline
builder.Services.AddClarcApplication<Program>(); // T is any type from your assembly
// ... rest of your configuration
2. Create a Simple Query and Handler
using MediatR;
using Odex.AspNetCore.Clarc.Application.CQRS;
public record GetUserQuery(int Id) : BaseQuery<UserResponse>;
public class GetUserQueryHandler : IRequestHandler<GetUserQuery, UserResponse>
{
public async Task<UserResponse> Handle(GetUserQuery request, CancellationToken cancellationToken)
{
// Fetch user from repository
var user = await _userRepository.GetByIdAsync(request.Id, cancellationToken);
return new UserResponse { Id = user.Id, Name = user.Name };
}
}
3. Create a Validator for the Query
using FluentValidation;
using Odex.AspNetCore.Clarc.Application.Validators;
public class GetUserQueryValidator : BaseValidator<GetUserQuery>
{
public GetUserQueryValidator()
{
RuleFor(x => x.Id).GreaterThan(0).WithMessage("User ID must be positive");
}
}
4. Send the Query from a Controller
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly IMediator _mediator;
public UsersController(IMediator mediator) => _mediator = mediator;
[HttpGet("{id}")]
public async Task<IActionResult> GetUser(int id)
{
var result = await _mediator.Send(new GetUserQuery(id));
return Ok(result);
}
}
Minimal API Example
app.MapGet("/users/{id}", async (IMediator mediator, int id) =>
{
var user = await mediator.Send(new GetUserQuery(id));
return Results.Ok(user);
});
β¨ Features
| Feature | Description |
|---|---|
| π§© CQRS Base Records | BaseCommand<TResponse>, BaseQuery<TResponse>, BaseResponse with timestamps. |
| π Paged Support | PagedQuery<TResponse> and PagedResponse<T> with readyβtoβuse pagination properties. |
| β Validation Pipeline | ValidationPipelineBehavior<TRequest,TResponse> β automatically validates requests using registered validators. |
| β οΈ Application Exceptions | DuplicateResourceException, OperationDeniedException, ServiceUnavailableException, ValidationException (collects multiple errors), etc. |
| π Simple Registration | AddClarcApplication<T>() extension method registers MediatR, validators, and the validation pipeline. |
ποΈ Core Components
1. CQRS Abstractions
BaseCommand<TResponse>
using MediatR;
namespace Odex.AspNetCore.Clarc.Application.CQRS;
public abstract record BaseCommand<TResponse> : IRequest<TResponse>
{
public DateTime ExecutedAt { get; init; } = DateTime.UtcNow;
}
BaseQuery<TResponse>
using MediatR;
namespace Odex.AspNetCore.Clarc.Application.CQRS;
public abstract record BaseQuery<TResponse> : IRequest<TResponse>
{
public DateTime RequestedAt { get; init; } = DateTime.UtcNow;
}
BaseResponse
namespace Odex.AspNetCore.Clarc.Application.CQRS;
public abstract record BaseResponse
{
public DateTime GeneratedAt { get; init; } = DateTime.UtcNow;
}
PagedQuery<TResponse>
namespace Odex.AspNetCore.Clarc.Application.CQRS;
public abstract record PagedQuery<TResponse> : BaseQuery<TResponse>
{
#region Pagination
public int Page { get; init; } = 1;
public int PageSize { get; init; } = 20;
#endregion
#region Sorting
public string? SortBy { get; init; }
public bool SortDescending { get; init; } = false;
#endregion
}
PagedResponse<T>
namespace Odex.AspNetCore.Clarc.Application.CQRS;
public abstract record PagedResponse<T> : BaseResponse
{
public required List<T> Items { get; init; }
public int TotalCount { get; init; }
public int Page { get; init; }
public int PageSize { get; init; }
public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
public bool HasPreviousPage => Page > 1;
public bool HasNextPage => Page < TotalPages;
}
2. Validation Pipeline
ValidationPipelineBehavior<TRequest, TResponse>
using FluentValidation;
using MediatR;
namespace Odex.AspNetCore.Clarc.Application.Behaviors;
public class ValidationPipelineBehavior<TRequest, TResponse>(IEnumerable<IValidator<TRequest>> validators)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
if (!validators.Any()) return await next(cancellationToken);
var context = new ValidationContext<TRequest>(request);
var validationResults = await Task.WhenAll(
validators.Select(v => v.ValidateAsync(context, cancellationToken))
);
var failures = validationResults
.SelectMany(result => result.Errors)
.Where(f => f != null)
.ToList();
if (failures.Count != 0)
throw new ValidationException(failures);
return await next(cancellationToken);
}
}
3. Application Exceptions
Exception Types Enum:
namespace Odex.AspNetCore.Clarc.Application.Enums;
public enum ExceptionType
{
Unknown,
Duplicate,
ServiceFailed,
ServiceUnavailable,
OperationDenied,
UnallowedOperation,
AccessDenied,
ValidationFailed
}
Base Exception:
using Odex.AspNetCore.Clarc.Application.Enums;
namespace Odex.AspNetCore.Clarc.Application.Exceptions;
public abstract class ApplicationException(string message, ExceptionType type)
: Exception(message)
{
public ExceptionType Type { get; } = type;
}
Concrete Exceptions:
| Exception | Code |
|---|---|
DuplicateResourceException |
[CODE HERE] |
OperationDeniedException |
[CODE HERE] |
OperationNotAllowedException |
[CODE HERE] |
ServiceInternalException |
[CODE HERE] |
ServiceUnavailableException |
[CODE HERE] |
UnauthorizedAccessException |
[CODE HERE] |
ValidationException |
[CODE HERE] |
4. Validators
BaseValidator<T>
using FluentValidation;
namespace Odex.AspNetCore.Clarc.Application.Validators;
public class BaseValidator<T> : AbstractValidator<T>;
PagedValidator<T>
using FluentValidation;
using Odex.AspNetCore.Clarc.Application.CQRS;
namespace Odex.AspNetCore.Clarc.Application.Validators;
public class PagedValidator<T> : AbstractValidator<PagedQuery<T>>
{
public PagedValidator()
{
RuleFor(x => x.Page).GreaterThanOrEqualTo(0);
RuleFor(x => x.PageSize).InclusiveBetween(1, 1000);
}
}
5. Service Registration Extension
AddClarcApplication<T>
using FluentValidation;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using Odex.AspNetCore.Clarc.Application.Behaviors;
namespace Odex.AspNetCore.Clarc.Application;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddClarcApplication<T>(this IServiceCollection services) where T : class
{
services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssemblyContaining<T>());
services.AddValidatorsFromAssemblyContaining<T>();
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationPipelineBehavior<,>));
return services;
}
}
π Usage Examples
Creating a Command
public record CreateUserCommand(string Name, string Email) : BaseCommand<UserResponse>;
Creating a Query Handler
public class GetUserQueryHandler : IRequestHandler<GetUserQuery, UserResponse>
{
private readonly IUserRepository _userRepository;
public GetUserQueryHandler(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public async Task<UserResponse> Handle(GetUserQuery request, CancellationToken cancellationToken)
{
var user = await _userRepository.GetByIdAsync(request.Id, cancellationToken);
if (user is null)
throw new NotFoundException(nameof(User), request.Id);
return new UserResponse(user.Id, user.Name, user.Email);
}
}
Creating a Validator
public class CreateUserCommandValidator : BaseValidator<CreateUserCommand>
{
public CreateUserCommandValidator()
{
RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
RuleFor(x => x.Email).NotEmpty().EmailAddress();
}
}
Using Paged Query
public record GetUsersPagedQuery : PagedQuery<PagedResponse<UserResponse>>;
public class GetUsersPagedQueryHandler : IRequestHandler<GetUsersPagedQuery, PagedResponse<UserResponse>>
{
public async Task<PagedResponse<UserResponse>> Handle(GetUsersPagedQuery request, CancellationToken ct)
{
var users = await _userRepository.GetPagedAsync(request.Page, request.PageSize, ct);
var total = await _userRepository.CountAsync(ct);
return new PagedResponse<UserResponse>
{
Items = users.Select(u => new UserResponse(u.Id, u.Name)).ToList(),
TotalCount = total,
Page = request.Page,
PageSize = request.PageSize
};
}
}
Throwing Application Exceptions
if (await _userRepository.EmailExistsAsync(command.Email))
throw new DuplicateResourceException("User", command.Email);
if (!_authorizationService.CanDelete(userId, currentUserId))
throw new UnauthorizedAccessException("DeleteUser", "Insufficient permissions");
try
{
await _emailService.SendAsync(user.Email);
}
catch (HttpRequestException)
{
throw new ServiceUnavailableException("EmailService");
}
π Namespace Map
| Namespace | Purpose |
|---|---|
Odex.AspNetCore.Clarc.Application.Behaviors |
ValidationPipelineBehavior |
Odex.AspNetCore.Clarc.Application.CQRS |
Base command/query/response records |
Odex.AspNetCore.Clarc.Application.Enums |
ExceptionType enum |
Odex.AspNetCore.Clarc.Application.Exceptions |
Application-specific exceptions |
Odex.AspNetCore.Clarc.Application.Validators |
Base validators and PagedValidator |
Odex.AspNetCore.Clarc.Application |
ServiceCollectionExtensions |
π Related Packages
- Odex.AspNetCore.Clarc.Domain β Domain layer with aggregates, events, and specifications.
- Odex.AspNetCore.Clarc.Infrastructure β Query builders, pagination, and infrastructure exceptions.
π€ Contributing
Contributions are welcome! Please follow the standard GitHub flow:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit changes (
git commit -m 'Add amazing feature') - Push to branch (
git push origin feature/amazing-feature) - Open a Pull Request
π License
This project is licensed under the MIT License β see the LICENSE file for details.
Built with β€οΈ for clean CQRS and validation on ASP.NET Core
| 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
- FluentValidation (>= 12.0.0)
- FluentValidation.DependencyInjectionExtensions (>= 12.0.0)
- MediatR (>= 13.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.