Kamersoft.Net.AspNetCore 8.0.0-preview.3

This is a prerelease version of Kamersoft.Net.AspNetCore.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package Kamersoft.Net.AspNetCore --version 8.0.0-preview.3                
NuGet\Install-Package Kamersoft.Net.AspNetCore -Version 8.0.0-preview.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="Kamersoft.Net.AspNetCore" Version="8.0.0-preview.3" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Kamersoft.Net.AspNetCore --version 8.0.0-preview.3                
#r "nuget: Kamersoft.Net.AspNetCore, 8.0.0-preview.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.
// Install Kamersoft.Net.AspNetCore as a Cake Addin
#addin nuget:?package=Kamersoft.Net.AspNetCore&version=8.0.0-preview.3&prerelease

// Install Kamersoft.Net.AspNetCore as a Cake Tool
#tool nuget:?package=Kamersoft.Net.AspNetCore&version=8.0.0-preview.3&prerelease                

Introduction

Provides with useful interfaces contracts in .Net 8.0 and some implementations mostly following the spirit of SOLID principles, CQRS... The library is strongly-typed, which means it should be hard to make invalid requests and it also makes it easy to discover available methods and properties though IntelliSense.

Feel free to fork this project, make your own changes and create a pull request.

Read the Kamersoft.Net.Samples for a minimal Web Api implementation with aggregates.

Getting Started

IOperationResult

Allows to create methods that return the status of an execution.

This interface contains all properties according to the result of the method execution. Some of those properties let you determine for example if the result instance is generic, a collection of errors, the status code or the value of the execution. The status code here is the one from the System.Net.HttpStatusCode. It contains methods to convert from non-generic interface to generic and vis-versa. The interface is useful if you want to return a result that can be analyzed even in a web environment by using some extensions that can automatically convert an IOperationResult to IResult.

The non generic interface has the following properties :

  • An object Result, a nullable property that qualifies or contains information about an operation return if available. You should call the method HasResult() before accessing the property to avoid a NullReferenceException.
  • An Uri LocationUrl, a nullable property that contains the URL mostly used with the status code Created in the web environment. You should call the method HasLocationUrl() before accessing the property to avoid a NullReferenceException.
  • A OperationHeaderCollection Headers property that contains a collection of headers if available. OperationHeaderCollection is a predefined record class that contains a collection of OperationHeader with useful methods.
  • An OperationErrorCollection Errors property that stores errors. Each error is a predefined ErrorElement struct which contains the error key and the error message and/or exceptions. OperationErrorCollection is a predefined record class with useful methods to add errors.
  • A HttpStatusCode StatusCode property that contains the status code of the execution. The status code from the System.Net.HttpStatusCode.
  • A boolean IsGeneric to determine whether or not the current instance is generic.
  • A boolean IsSuccess and IsFailure to determine whether or not the operation is a success or a failure.
  • A T? IsResultOfType() is a generic that returns the Result as the T parameter type if possible or null value.
  • A TException IsException() is a generic method that returns the exception found in the Errors as TException if available.

The generic interface overrides the object Result to TResult type.

Create a method that returns an IOperationResult

public IOperationResult CheckThatValueIsNotNull(string? value)
{
    if(string.IsNullOrEmpty(value))
    {
        return OperationResults
            .BadRequest()
            .WithError(nameof(value), "value can not be null")
            .Build();
    }

    return OperationResults
        .Ok()
        .Build();
}

The method returns a class that implements the IOperationResult interface. To do so, you can use the specific extension method according to your needs :

  • OperationResults which is a factory to create specifics results from Ok to InternalServerError.

Each extension method allows you to add errors, headers, Uri or a value to the target operation result. The key here in error can be the name of the member that has the error. The caller of this method can check if the return operation is a success or a failure result.

When used in an Asp.Net Core application, you will need to add the Kamersoft.Net.AspNetCore NuGet package that will provides helpers to automatically manage IResult responses.

[HttpGet]
public IResult GetUserByName(string? name)
{
    if(CheckThatValueIsNotNull(name) is { isFailure : true} failure)
        return failure.ToMinimalResult();

    // ...get the user
	IOperationResult result = DoGetUser(...);
	
    return result.ToMinimalResult();
}

In this case, if the name is null, the operation result from the method will be converted to an implementation of IResult using the extension method ToMinimalResult, that will produce a perfect response with all needed information.

You can also use the OperationResultException to throw a specific exception that contains a failure IOperationResult when you are not able to return an IOperationResult instance. All the operation result instance are serializable with a specific case for Asp.Net Core application, the produced response Content will contains the serialized Result property value if available in the operation result. You will find the same behavior for all the interface that use the IOperationResult in their method as return value such as : ICommandHandler< TCommand >, IQueryHandler< TQuery, TResult >, IDomainEventHandler< TDomainEvent > ...

Decorator pattern

You can use the extension methods to apply the decorator pattern to your types.

This method and its extensions ensure that the supplied TDecorator" decorator is returned, wrapping the original registered "TService", by injecting that service type into the constructor of the supplied "TDecorator". Multiple decorators may be applied to the same "TService". By default, a new "TDecorator" instance will be returned on each request, independently of the lifestyle of the wrapped service. Multiple decorators can be applied to the same service type. The order in which they are registered is the order they get applied in. This means that the decorator that gets registered first, gets applied first, which means that the next registered decorator, will wrap the first decorator, which wraps the original service type.

 services.XTryDecorate<TService, TDecorator>();   

Suppose you have a command and a command handler defined like this :

public sealed record AddPersonCommand : ICommand;

public sealed class AddPersonCommandHandler : ICommandHandler<AddPersonCommand>
{
    public ValueTask<IOperationResult> HandleAsync(
        AddPersonCommand command, CancellationToken cancellationToken = default)
    {
        // your code ...

        return OperationResults.Ok().Build();
    }
}

Suppose you want to add logging for the AddPersonCommandHandler, you just need to define the decorator class that will use the logger and the handler.

public sealed class AddPersonCommandHandlerLoggingDecorator : 
    ICommandHandler<AddPersonCommand>
{
    private readonly ICommandHandler<AddPersonCommand> _decoratee;
    private readonly ILogger<AddPersonCommandHandler> _logger;
    
    public AddPersonCommandHandlerLoggingDecorator(
        ILogger<AddPersonCommandHandler> logger,
        ICommandHandler<AddPersonCommand> decoratee)
        => (_logger, _decoratee) = (logger, decoratee);

    public async ValueTask<OperationResult> HandleAsync(
        AddPersonCommand command, CancellationToken cancellationToken = default)
    {
        _logger.Information(...);
        
        var response = await _decoratee
            .HandleAsync(command, cancellationToken)
            .configureAwait(false);
        
        _logger.Information(...)
        
        return response;
    }
}

And to register the decorator, you just need to call the specific extension method :

services
    .AddXHandlers()
    .XTryDecorate<AddPersonCommandHandler, AddPersonCommandHandlerLoggingDecorator>();

Sometimes you want to use a generic decorator. You can do so for all commands that implement ICommand interface or something else.

public sealed class CommandLoggingDecorator<TCommand> : ICommandHandler<TCommand>
    where TCommand : notnull, ICommand // you can add more constraints
{
    private readonly ICommandHandler<TCommand> _ decoratee;
    private readonly ILogger<TCommand> _logger;
    
    public CommandLoggingDecorator(
        ILogger<TCommand> logger, ICommandHandler<TCommand> decoratee)
        => (_logger, _ decoratee) = (logger, decoratee);

    public async ValueTask<OperationResult> HandleAsync(
         TCommand command, CancellationToken cancellationToken = default)
    {
        _logger.Information(...);
        
        var response = await _decoratee
            .HandleAsync(command, cancellationToken).configureAwait(false);
        
        _logger.Information(...)
        
        return response;
    }
}

And for registration the CommandLoggingDecorator will be applied to all command handlers whose commands meet the decorator's constraints : To be a notnull and implement ICommand interface.

services
    .AddXHandlers()
    .XTryDecorate(typeof(ICommandHandler<>), typeof(CommandLoggingDecorator<>));

CQRS Pattern

CQRS stands for Command and Query Responsibility Segregation, a pattern that separates read and update operations for a data store.

The following interfaces are used to apply command and query operations :

public interface IQuery<TResult> {}
public interface IAsyncQuery<TResult> {}
public interface ICommand {}

public interface IQueryHandler<TQuery, TResult>
    where TQuery : notnull, IQuery<TResult> 
{
    ValueTask<IOperationResult<TResult>> HandleAsync(
        TQuery query, CancellationToken cancellationToken = default);
}
public interface IAsyncQueryHandler<TQuery, TResult>
    where TQuery : notnull, IAsyncQuery<TResult> 
{
    IAsyncEnumerable<TResult> HandleAsync(
        TQuery query, CancellationToken cancellationToken = default);
}

public interface ICommandHandler<TCommand>
    where TCommand : notnull, ICommand
{
    ValueTask<IOperationResult> HandleAsync(
        TCommand command, CancellationToken cancellationToken = default);
}

So let's create a command and its handler. A command to add a new product for example.

public sealed record AddProductCommand(
    [property : StringLength(byte.MaxValue, MinimumLength = 3)] string Name,
    [property : StringLength(short.MaxValue, MinimumLength = 3)] string Description) :
    ICommand, IPersistenceDecorator;

ICommand already contain an Id property of type Guid and the IPersistenceDecorator interface is to allow the command to be persisted at the end of the control flow when there is no exception. Entity is a base class that contains common properties for entities.

public sealed class AddProductCommandHandler : ICommandHandler<AddProductCommand>
{
    private readonly ProductContext _uow;
    public AddProductCommandHandler(ProductContext uow) => _uow = uow;

    public async ValueTask<IOperationResult> HandleAsync(
        AddProductCommand command, CancellationToken cancellationToken)
    {
        // create the new product instance : 'With' is static method to build a product
        var product = Product.With(command.Id, command.Name, command.Description);

        // insert the new product in the collection of products
        await _uow.Products.AddAsync(product, cancellationToken).ConfigureAwait(false);
    }
}

The validation of the command, the validation of command duplication and persistence will happen during the control flow using decorators.

public sealed class AddProductCommandValidator<AddProductCommand> :
    Validator<AddProductCommand>
{
     private readonly ProductContext _uow;
    public AddProductCommandValidator(ProductContext uow, IServiceProvider sp)
        :base(sp) => _uow = uow;

     public async ValueTask<IOperationResult> ValidateAsync(AddProductCommand argument)
    {
        // validate the command using attributes
       if(Validate(command) is { isFailure : true } failure)
            return failure;

        // check for duplication
        // You can stop here because if a duplication error occurs while saving, 
        // the final operation result will contain this error.

        // this is just for demo
        // we just need to know if a record with
        // the specified id already exist
        var isFound = await _uow.Products.CountAsync(x=>x.Id == command.Id,
            cancellationToken).ConfigureAwait(false) > 0;

        if( isFound ) // duplicate
        {
            // the result can directly be used in a web environment
            return OperationResults
                .Conflict()
                .WithError(nameof(command.Id), $"Command identifier '{command.Id}' already exist")
                .Build();
        }

        return OperationResults.Ok().Build();
    }
}

And now let's create a query and its handler to request a product.

public readonly record struct ProductDTO(string Id, string Name, string Description);

public sealed record GetProductQuery(Guid Id) : IQuery<ProductDTO?>;

// You can use a class and apply a filter directly on that class :
public sealed record GetProductQuery(Guid Id) : 
    QueryExpression<Product>, IQuery<ProductDTO?>
{
    public override Expression<Func<Product, bool>> GetExpression()
        => x => x.Id == Id;
}

public sealed class GetProductQueryHandler : 
    IQueryHandler<GetProductQuery, ProductDTO?>
{
    private readonly ProductContext _uow;

    public GetProductQueryHandler(ProductContext uow) => _uow = uow;

    public async ValueTask<IOperationResult<ProductDTO?>> HandleAsync(
        GetProductQuery query, CancellationToken cancellationToken = default)
    {
        // You can make a search using a query or the key

        if( await _uow.Products
            .Where(query)
            .OrderBy(o => o.Id)
            .Select(s => new ProductDTO(x.Id, x.Name, x.Description))
            .FirstOrDefaultAsync(canellationToken)
            .ConfigureAwait(false)
            is { } productDTO)
        {
            return OperationResults
                .Ok<ProductDTO?>(productDTO)
                .Build();
        }

        // create a key for search --------------
        var key = ProductId.With(query.Id);

        if(await _uow.Products
            .FindAsync(key, cancellationToken)
            .ConfigureAwait(false) is { } product)
        {
            ProductDTO productDTO = new(product.Id, product.Name, product.Description);
            return OperationResults
                .OkResult<ProductDTO?>()
                .WithResult(productDTO)
                .Build();
        }

        return OperationResults.NotFound<ProductDTO?>().Build();
    }
}

Finally, we need to use the dependency injection to put it all together :

var serviceProvider = new ServiceCollection()
    .AddXDataContext<ProductContext>(define options)
    .AddXHandlers(
        options => options.UsePersistenceDecorator().UseValidationDecorator())
    .AddXDispatcher()
    .BuildServiceprovider();

    // Add a product
    var dispatcher = serviceProvider.GetRequiredService<IDispatcher>();
    var addProduct = new AddProductCommand("Kamersoft 8", "Kamersoft.Net Library");
    IOperationResult result = await dispatcher
        .SendAsync(addProduct).ConfigureAwait(false);

    // check the result
    ...

The AddXDataContext registers the specified data context using the options provided.
The AddXHandlers registers all handlers found in the executing application, and apply persistence decorator and validation decorator to all the commands according to the constraints.
The AddXDispatcher registers the internal implementation of IDispatcher to resolve handlers.

Features

Usually, when registering types, we are forced to reference the libraries concerned and we end up with a very coupled set. To avoid this, you can register these types by calling an export extension method, which uses MEF: Managed Extensibility Framework.

In your api program class

AddXServiceExport(IConfiguration, Action{ExportServiceOptions}) adds and configures registration of services using the IAddServiceExport interface implementation found in the target libraries according to the export options. You can use configuration file to set up the libraries to be scanned.

    ....
    builder.Services
        .AddXServiceExport(
            Configuration, 
            options => options.SearchPattern = "your-search-pattern-dll");
    ...

In the library you want types to be registered

[Export(typeof(IAddServiceExport))]
public sealed class RegisterServiceExport : IAddServiceExport
{
    public void AddServices(IServiceCollection services, IConfiguration configuration)
    {
        // you can register your services here
        ....
    }
}

IAggregate

Libraries also provide with DDD model implementation IAggregate< TAggregateId> using event sourcing and out-box pattern.

Product 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. 
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
8.0.0-rc.1.1 73 9/19/2023
8.0.0-rc.1 78 9/14/2023
8.0.0-preview-2 92 7/2/2023
8.0.0-preview-1 80 6/10/2023
8.0.0-preview.3 83 9/8/2023

Kamersoft.Net.