Xpandables.Net.BlazorExtended 6.0.4

.NET 6.0
Install-Package Xpandables.Net.BlazorExtended -Version 6.0.4
dotnet add package Xpandables.Net.BlazorExtended --version 6.0.4
<PackageReference Include="Xpandables.Net.BlazorExtended" Version="6.0.4" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Xpandables.Net.BlazorExtended --version 6.0.4
The NuGet Team does not provide support for this client. Please contact its maintainers for support.
#r "nuget: Xpandables.Net.BlazorExtended, 6.0.4"
#r directive can be used in F# Interactive, C# scripting and .NET Interactive. Copy this into the interactive tool or source code of the script to reference the package.
// Install Xpandables.Net.BlazorExtended as a Cake Addin
#addin nuget:?package=Xpandables.Net.BlazorExtended&version=6.0.4

// Install Xpandables.Net.BlazorExtended as a Cake Tool
#tool nuget:?package=Xpandables.Net.BlazorExtended&version=6.0.4
The NuGet Team does not provide support for this client. Please contact its maintainers for support.


Provides with useful interfaces contracts in .Net 6.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.

Here are some examples of use :

Web Api using CQRS and EFCore

Add the following nuget packages to your project :



Model Definition

// Entity is the domain object base implementation that provides with an Id,
// You can use Aggregate{TAggregateId} if you're targeting DDD.

public sealed class PersonEntity : Entity
    public string FirstName { get; private set; }
    public string LastName { get; private set; }
    public static PersonEntity NewPerson(Guid id, string firstName, string lastName)
        // custom check
        return new(id, fistName, lastname);
    public void ChangeFirstName(string firstName)
        => FirstName = firstName ?? throw new ArgumentNullException(nameof(firstName));
    public void ChangeLastName(string lastName)
        => LastName = lastName ?? throw new ArgumentNullException(nameof(lastName));
    private PersonEntity(Guid id, string firstName, string lastName)
        => (Id, FirstName, Lastname) = (id, firstName, lastName);

Contract definition

// Contract is decorated with HttpClientAttribute that describes the parameters for a request 
// used with IHttpClientDispatcher, where IHttpClientDispatcher provides with methods to handle HTTP
// Rest client queries and commands using a typed client HTTP Client. It also allows, in a .Net environment,
// to no longer define client actions because they are already included in the contracts, 
// by implementing interfaces such as IHttpRequestPathString, IHttpRequestFormUrlEncoded,
// IHttpRequestMultipart, IHttpRequestString, IHttpRequestStream...
// Without the use of one of those interfaces, the whole class will be serialized.

[HttpClient(Path = "api/person", In = ParameterLocation.Body, Method = ParameterMethod.Post, IsSecured = false)]
public readonly record struct AddPersonRequest([Required] Guid Id, [Required] string FirstName, [Required] string LastName)
    : IHttpClientRequest, IHttpRequestString
    // You can omit the use of IHttpRequestString, the whole class will be serialised.
    public object GetStringContent() => new { Id, FirstName, LastName };

[HttpClient(Path = "api/person/{id}", IsNullable = true, In = ParameterLocation.Path, 
    Method = ParameterMethod.Get, IsSecured = false)]
public readonly record struct GetPersonRequest([Required] Guid Id) : IHttpClientRequest<Person>,
    public IDictionary<string, string> GetPathStringSource()
        => new Dictionary<string, string> { { nameof(Id), Id.ToString() } };

public readonly record struct Person(Guid Id, string FirstName, string LastName);

Command/Query and Handler definitions

// The command must implement the ICommand interface and others to enable their behaviors.
// Such as IValidatorDecorator to apply validation before processing the command,
// IPersistenceDecorator to add persistence to the control flow
// or IInterceptorDecorator to add interception of the command process...

// You can derive from QueryExpression{TClass} to allow command/query to behave like an expression
// when querying data, and override the target method.

public readonly record struct AddPersonCommand(
    Guid Id, string FirstName, string LastName) : ICommand, IValidatorDecorator;

public readonly record struct GetPersonQuery(Guid Id) :  IQuery<Person>;

public sealed class AddPersonCommandHandler : ICommandHandler<AddPersonCommand>
    // We do not use a data context here but a delegate that makes thing more simple for test.

    private readonly Func<PersonEntity, ValueTask> _savePerson;
    public AddPersonCommandHandler(Func<PersonEntity, ValueTask> savePerson)
        => _savePerson = savePerson;
    public async ValueTask<OperationResult> HandleAsync(AddPersonCommand command, 
        CancellationToken cancellationToken = default)
        // You can check here for data validation or use a specific class for that
        // (see AddPersonCommandValidationDecorator).
        var newPerson = Person.NewPerson(
        await _savePerson(newPerson).configureAwait(false);
        // The return result

        return OperationResult
            .AddHeaders("newId", newPerson.Id);

        // or you can use the OperationResult.Created(...) response.
        // Ok, NotFound... are extension methods that return an OperationResult that acts like
        // IActionResult in AspNetCore.
        // The OperationResultFilter will process the output message format.
        // You can add a decorator class to manage the exception.

public sealed class GetPersonQueryHandler : IQueryHandler<GetPersonQuery, Person>
    private readonly Func<Guid, CancellationToken, ValueTask<PersonEntity?>> _getPerson;
    public GetPersonQueryHandler(Func<Guid, CancellationToken, ValueTask<PersonEntity?>> getPerson)
        => _getPerson = getPerson;    
    public override async ValueTask<OperationResult<Person>> HandleAsync(GetPersonQuery query,
        CancellationToken cancellationToken = default)
        var result = await _getPerson(query.Id, cancellationToken).configureAwait(false);
        return result switch
            PersonEntity person => OperationResult.Ok(new Person(person.Id, person.FirstName, person.LastName)),
            _ => OperationResult.NotFound<Person>(nameof(query.Id), "Id not found."))

// When using validation decorator.
// IValidator{T} defines a method contract used to validate a type-specific argument using a decorator.
// The validator get called during the control flow before the handler.
// If the validator returns a failed operation result or throws exception, the execution will be interrupted
// and the result of the validator will be returned.
// We consider as best practice to handle common conditions without throwing exceptions
// and to design classes so that exceptions can be avoided.

public sealed class AddPersonCommandValidationDecorator : IValidator<AddPersonCommand>
    private readonly Func<Guid, ValueTask<bool>> _personExists;
    public AddPersonCommandValidationDecorator(Func<Guid, ValueTask<bool>> personExists)
        => _personExists = personExists;
    public async ValueTask<OperationResult> Validate(AddPersonCommand argument)
        return await _personExists(argument.Id) switch
            true => OperationResult.Conflict(nameof(argument.Id), "Id already exist"),
            _ => OperationResult.Ok()

Context definition

// We are using EFCore

public sealed class PersonEntityTypeConfiguration : IEntityTypeConfiguration<PersonEntity>
    public void Configure(EntityTypeBuilder<PersonEntity> builder)
        builder.HasKey(p => p.Id);
        builder.Property(p => p.FirstName);
        builder.Property(p => p.LastName);

// DataContext is an abstract class that inherits from DbContext (EFCore) and adds some behaviors.

public sealed class PersonContext : DataContext
     public PersonContext(DbContextOptions<PersonContext> contextOptions)
        : base(contextOptions) { }
     protected override void OnModelCreating(ModelBuilder modelBuilder)
         modelBuilder.ApplyConfiguration(new PersonEntityTypeConfiguration());
     public DbSet<PersonEntity> People { get; set; } = default!;

Controller definition

// IDispatcher provides with methods to discover registered handlers at runtime.
// We consider as best practice to return ValidationProblemDetails/ProblemDetails in case of errors

public class PersonController : ControllerBase
    private readonly IDispatcher _dispatcher;
    public PersonController(IDispatcher dispatcher) 
       => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));

    [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ValidationProblemDetails))]
    public async Task<IActionResult> AddPersonAsync(
        [FromBody] AddPersonRequest request, CancellationToken cancellationToken = default)
        var command = new AddPersonCommand(request.Id, request.FirstName, request.LastName);

        OperationResult result = await _dispatcher
            .SendAsync(command, cancellationToken)

        return result.ToActionOperationResult();
    [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(ValidationProblemDetails))]
    [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Person))]
    public async Task<IActonResult> GetPersonAsync(
        [FromRoute] GetPersonRequest request, cancellationToken cancellationToken = default)
        var query = new GetPersonQuery(request.Id);

        OperationResult<Person> personResult = await _dispatcher
            .FetchAsync(query, cancellationToken)

        return personResult.ToActionOperationResult();

    // ...

// The startup class
// We will register handlers, context, validators and decorators.

public static class EntityExtensions
    internal static bool EntityExists<TEntity, TentityId>(
        this DataContext dataContext, TEntityId entityId)
        where TEntity : Entity
        where TEntityId : notnul
        => dataContext.Find<TEntity>(new object[] { entityId}) is not null;

    internal static async ValueTask<TEntity?> FindEntity<TEntity, TEntityId>(
        this DataContext dataContext, 
        TEntityId entittyId, CancellationToken cancellationToken = default)
        where TEntity : Entity
        where TEntityId : notnull
        => await dataContext.FindAsync<TEntity>(
            new object[] { entittyId }, cancellationToken).ConfigureAwait(false);

    internal static async ValueTask AddAndSaveEntity<TEntity>(
        this DataContext dataContext, TEntity entity, CancellationToken cancellationToken = default)
        where TEntity : Entity
        await dataContext.AddAsync(entity, cancellationToken).ConfigureAwait(false);
        await dataContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);

public sealed class Startup
    public void ConfigureServices(IServiceCollection services)
        // common registrations...
            .AddXDataContext<PersonContext>(Servicelifetime.Scoped, options => options.UseInMemoryDatabase(nameof(PersonContext))
                options =>
            .AddXPersistenceDecoratorDelegate(provider =>
                var context = provider.GetRequiredService<PersonContext>();
                return context.SaveChangesAsync;

        // add delegates
        services.AddScoped<Func<Guid, bool>>(provider =>
            provider.GetRequiredService<PersonContext>().EntityExists<PersonEntity, Guid>);

        services.AddScoped<Func<Guid, CancellationToken, ValueTask<PersonEntity?>>>(provider =>
            provider.GetRequiredService<PersonContext>().FindEntity<PersonEntity, Guid>);

        services.AddScoped<Func<PersonEntity, ValueTask>>(provider =>

        // AddXpandableServices() will make available methods for registration
        // AddXOperationResultExceptionMiddleware() handles the OperationResult exception
        // AddXOperationResultConfigureJsonOptions() will add operation result converters
        // AddXOperationResultConfigureMvcOptions() will add operation result filters        
        // AddXDataContext{TContext} registers the TContext
        // AddXDispatcher() registers the dispatcher
        // AddXHandlerProvider() registers a handlers provider
        // AddXHandlers(serviceLifetime, options, assemblies) registers all handlers and associated classes (validators, decorators...)
        // according to the options set.
        // ...
    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

Wep Api Test class

We are using the MSTest template project. You can use another one. Add this package to your test project : Microsoft.AspNetCore.Mvc.Testing reference to your api test project.

[DataRow("My FirstName", "My LastName")
public async Task AddPersonTestAsync(string firstName, string lastName)
    // Build the api client
    Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
    var factory = new WebApplicationFactory<Program>(); // from the api
    var client = factory.CreateClient();
    // if you get serialization error (due to System.Text.Json), you can
    // set the serialization options by using an extension method or by globally
    // setting the IHttpClientDispatcher.SerializerOptions property.
    using var httpClientDispatcher = new HttpClientDispatcher(
        new HttpClientRequestBuilder(),
        new HttpClientResponseBuilder(),

    var addPersonRequest = new AddPersonRequest(Guid.NewGuid(), firstName, lastName);
    using var response = await httpClientDispatcher.SendAsync(addPersonRequest).ConfigureAwait(false);

    if (!response.IsValid())
         OperationResult operationResult = response.ToOperationResult();
         // ToOperationResult() is an extension method for HttpRestClientResponse that returns
         // an OperationResult from the response.
         foreach (OperationError error in operationResult.Errors)         
            Trace.WriteLine($"Key : {error.Key}");
        string createdId = response.Headers["newId"];
        Trace.WriteLine($"Added person : {createdId}");

Blazor WebAss with Web Api using IHttpClientDispatcher

Blazor WebAss project

Add the following nuget packages : Xpandables.Net.BlazorExtended

In the Program file, replace the default code with this.

public class Program
    public static async Task Main(string[] args)
       var builder = WebAssemblyHostBuilder.CreateDefault(args);
                .AddXHttpClientDispatcher(httpClient =>
                    httpClient.BaseAddress = new Uri("https://localhost:44396"); // your api url
                        .Add(new MediaTypeWithQualityHeaderValue(ContentType.Json));
        // AddXHttpClientDispatcher(httpClient) will add the IHttpClientDispatcher
        // implementation using the HttpClient with your configuration.
        // if you get errors with System.Text.Json, you can use IHttpClientDispatcher.SerializerOptions
        // property to globally set the serializer options or use extension methods in your code.
        // custom code...
        await builder.Build().RunAsync();


<EditForm Model="@model" OnValidSubmit="AddSubmitAsync">

    <XDataAnnotationsValidator @ref="@Validator" />

    <div class="form-group">
        <label for="FirstName" class="-form-label">First Name</label>
        <XInputText @bind-Value="model.FirstName" type="text" class="form-control" />
        <ValidationMessage For="@(() => model.FirstName)" />

    <div class="form-group">
        <label for="LastName" class="col-form-label">Last Name</label>
        <XInputText @bind-Value="model.LastName" type="text" class="form-control" />
        <ValidationMessage For="@(() => model.LastName)" />

    <div class="form-group">
        <div class="col-md-12 text-center">
            <button class="col-md-12 btn btn-primary">


XInputText is a component that allows text to be validated on input.

XDataAnnotationsValidator is a DataAnnotationsValidator derived class that allows insertion of external errors to the edit context.


public sealed class PersonModel
    public string FirstName { get; set; } = default!;
    public string LastName { get; set; } = default!;

public partial class AddPerson
    protected XDataAnnotationsValidator Validator { get; set; } = default!;
    protected IHttpClientDispatcher HttpClientDispatcher { get; set; } = default!;
    private readonly PersonModel model = new();
    protected async Task AddSubmitAsync()
        // You can use the AddPersonRequest from the api or create another class
        // We do not specify the action here because the AddPersonRequest definition
        // already hold all the necessary information.
        var addRequest = new AddPersonRequest(Guid.NewGuid(), model.FirstName, model.LastName);
        using var addResponse = await HttpClientDispatcher.SendAsync(addRrequest).ConfigureAwait(false);

        OperationResult operationResult = addResponse.ToOperationResult();

        if (addResponse.IsValid()) // or operationResult.IsSuccess
            // custom code like displaying the result
            var createdId = operationResult.Headers["newId"];


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 startup 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.

public class Startup
            .AddXServiceExport(Configuration, options => options.SearchPattern = "your-search-pattern-dll")

In the library you want types to be registered

public sealed class RegisterServiceExport : IAddServiceExport
    public void AddServices(IServiceCollection services, IConfiguration configuration)

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.

        .XTryDecorate<TService, TDecorator>()

Suppose you want to add logging for the AddPersonCommand ...

// The AddPersonCommand decorator for logging

public sealed class AddPersonCommandHandlerLoggingDecorator : 
    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)
        var response = await _decoratee.HandleAsync(command, cancellationToken).configureAwait(false);
        return response;

// Registration

        .XTryDecorate<AddPersonCommandHandler, AddPersonCommandHandlerLoggingDecorator>()

 // or

        options =>
            options.UseValidatorDecorator(); // this option will add the command decorator registration

// or you can define the generic model, for all commands that implement ICommand 
// interface or something else.

public sealed class CommandLoggingDecorator<TCommand> : ICommandHandler<TCommand>
    where TCommand : class, 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<TResult>> HandleAsync(
         TCommand command, CancellationToken cancellationToken = default)
        var response = await _decoratee.HandleAsync(command, cancellationToken).configureAwait(false);
        return response;

// and for registration

// The CommandLoggingDecorator will be applied to all command handlers whose commands meet 
// the decorator's constraints : 
// To be a class and implement ICommand interface

        .XTryDecorate(typeof(ICommandHandler<>), typeof(CommandLoggingDecorator<>))


Libraries also provide with DDD model implementation 'Aggregate{TAggregateId}' using event sourcing and out-box pattern.

Product Versions
.NET net6.0 net6.0-android net6.0-ios net6.0-maccatalyst net6.0-macos net6.0-tvos net6.0-windows
Compatible target framework(s)
Additional computed target framework(s)
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
6.0.4 67 3/15/2022
6.0.3 64 2/22/2022
6.0.2 87 1/4/2022
6.0.1 103 12/4/2021
6.0.0 140 11/8/2021
6.0.0-rc.4.3 77 11/3/2021
6.0.0-rc.3.1 90 10/15/2021
6.0.0-rc.3 81 10/14/2021
6.0.0-rc.2 75 9/21/2021
6.0.0-preview.5 80 8/26/2021
5.6.1 203 6/30/2021
5.6.0 169 6/9/2021
5.5.1 162 5/26/2021

Fix Model binding for struct and nullable types