Xpandables.Net.Blazor 7.3.3

dotnet add package Xpandables.Net.Blazor --version 7.3.3                
NuGet\Install-Package Xpandables.Net.Blazor -Version 7.3.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="Xpandables.Net.Blazor" Version="7.3.3" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Xpandables.Net.Blazor --version 7.3.3                
#r "nuget: Xpandables.Net.Blazor, 7.3.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 Xpandables.Net.Blazor as a Cake Addin
#addin nuget:?package=Xpandables.Net.Blazor&version=7.3.3

// Install Xpandables.Net.Blazor as a Cake Tool
#tool nuget:?package=Xpandables.Net.Blazor&version=7.3.3                

Xpandables.Net

Provides with useful interfaces contracts in .Net 7.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 Xpandables.Net.Samples for a minimal Web Api implementation using multi-tenancy with aggregates.

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 FluentOperationResults
            .BadRequest()
            .WithError(nameof(value), "value can not be null")
            .Create();
    }

    return FluentOperationResults.Ok().Create();
}

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

  • FluentOperationResults with is factory using fluent interface to create specifics results from Ok to InternalServerError.
  • OperationResults which is a factory to create specifics results from Ok to InternalServerError.
  • TypedOperationResults which is also a factory to create only a success or a failure operation.

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 Xpandables.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 FluentOperationResults.Ok().Create();
    }
}

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);
}

public interface IDispatcher : IServiceProvider
{
    ValueTask<IOperationResult> SendAsync<TCommand>(
        TCommand command, CancellationToken cancellationToken = default)
        where TCommand : notnull, ICommand;

    ValueTask<IOperationResult<TResult>> GetAsync<TQuery, TResult>(
        TQuery query, CancellationToken cancellationToken = default)
        where TQuery : notnull, IQuery<TResult>;

    IAsyncEnumerable<TResult> FetchAsync<TQuery, TResult>(
        TQuery query, CancellationToken cancellationToken = default)
        where TQuery : notnull, IAsyncQuery<TResult>;
}

public interface IRepository<TEntity>
{
    ...
}

public interface IUnitOfWork
{
    ValueTask<int> PersistAsync(CancellationToken cancellationToken = default);
    IRepository<TEntity> GetRepository<TEntity>() where TEntity : class, IEntity;
    ...
}

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

public sealed record class 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 IUnitOfWork _uow;
    public AddProductCommandHandler(IUnitOfWork uow) => _uow = uow;

    public async ValueTask<IOperationResult> HandleAsync(
        AddProductCommand command, CancellationToken cancellationToken)
    {
        // get the target repository
        IWriteRespository<Product> repository = _uow.GetWriteRepository<Product>();

        // you can use the extension method to get the repo 
        // from the collection of services :
        IWriteRespository<Product> repository = 
            _uow.GetWriteRepositoryFromServices<Product>();

        // 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 repository.InsertAsync(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 AddProductCommandValidationDecorator<AddProductCommand> :
    Validator<AddProductCommand>
{
     private readonly IUnitOfWork _uow;
    public AddProductCommandValidationDecorator(IUnitOfWork 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
        // get the read repository
        IReadRespository<Product> repository = _uow.GetReadRepository<Product>();

        // create the filter for search
        var filter = new EntityFilter<Product>
        {
            Criteria = x => x.Id == command.Id
        };

        // apply the filter, we just need to know if a record with
        // the specified id already exist
        var isFound = await repository.CountAsync(
            filter, cancellationToken).ConfigureAwait(false) > 0;

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

        return OperationResults.Ok();
    }
}

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

public sealed record 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 class 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 IUnitOfWork _uow;
    public GetProductQueryHandler(IUnitOfWork uow) => _uow = uow;

    public async ValueTask<IOperationResult<ProductDTO?>> HandleAsync(
        GetProductQuery query, CancellationToken cancellationToken = default)
    {
        // get the read repository
        IReadRespository<Product> repository = _uow.GetReadRepository<Product>();

        // You can make a search using a filter or the key

        // create the filter for search --------------
        var filter = new EntityFilter<Product, ProductDTO?>
        {
            // this is because GetProductQuery can be converted to an expression
            Criteria = query,
            OrderBy = x => x.OrderBy(o => o.Id),
            Selector = x => new(x.Id, x.Name, x.Description), // projection
            Paging = Paging.With(0, 1) // only the first result, this is optional
        };

        if( await repository.TryFindAsync(filter, cancellationToken).ConfigureAwait(false)
            is { } productDTO)
        {
            return FluentOperationResults
                .OkResult<ProductDTO?>()
                .WithResult(productDTO)
                .Create();
        }

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

        if(await repository
            .TryFindAsync(key, cancellationToken)
            .ConfigureAwait(false) is { } product)
        {
            ProductDTO productDTO = new(product.Id, product.Name, product.Description);
            return FluentOperationResults
                .OkResult<ProductDTO?>()
                .WithResult(productDTO)
                .Create();
        }

        return OperationResults.NotFoundResult<ProductDTO?>();
    }
}

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

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

    // Add a product
    var dispatcher = serviceProvider.GetRequiredService<IDispatcher>();
    var addProduct = new AddProductCommand("Xpandables 7", "Xpandables.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 AddXUnitOfWorkFactoryContext registers the IUnitOfWork for the specified data context.
The AddXRepositoryFor adds the repository to the collection of services.
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 net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  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
7.3.3 181 5/9/2023
7.1.4 247 2/26/2023
7.1.3 266 2/19/2023
7.0.0 348 11/9/2022
7.0.0-rc2.0.1 97 10/12/2022
7.0.0-rc1.0.0 125 9/26/2022

Fix snapshot aggregate behavior.