Odex.AspNetCore.Clarc.Application 0.1.0

There is a newer version of this package available.
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
                    
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="Odex.AspNetCore.Clarc.Application" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Odex.AspNetCore.Clarc.Application" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Odex.AspNetCore.Clarc.Application" />
                    
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 Odex.AspNetCore.Clarc.Application --version 0.1.0
                    
#r "nuget: Odex.AspNetCore.Clarc.Application, 0.1.0"
                    
#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 Odex.AspNetCore.Clarc.Application@0.1.0
                    
#: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=Odex.AspNetCore.Clarc.Application&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Odex.AspNetCore.Clarc.Application&version=0.1.0
                    
Install as a Cake Tool

βš™οΈ Odex.AspNetCore.Clarc.Application

NuGet Version NuGet Downloads License: MIT

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

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

  • 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:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Commit changes (git commit -m 'Add amazing feature')
  4. Push to branch (git push origin feature/amazing-feature)
  5. 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 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

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.0 92 5/15/2026
0.1.1 110 4/29/2026
0.1.0 107 4/29/2026