Xpandables.Net.BlazorExtended
6.0.2
See the version list below for details.
dotnet add package Xpandables.Net.BlazorExtended --version 6.0.2
NuGet\Install-Package Xpandables.Net.BlazorExtended -Version 6.0.2
<PackageReference Include="Xpandables.Net.BlazorExtended" Version="6.0.2" />
paket add Xpandables.Net.BlazorExtended --version 6.0.2
#r "nuget: Xpandables.Net.BlazorExtended, 6.0.2"
// Install Xpandables.Net.BlazorExtended as a Cake Addin #addin nuget:?package=Xpandables.Net.BlazorExtended&version=6.0.2 // Install Xpandables.Net.BlazorExtended as a Cake Tool #tool nuget:?package=Xpandables.Net.BlazorExtended&version=6.0.2
Xpandables.Net
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 :
Xpandables.Net.AspNetCore
Xpandables.Net.EntityFramework
Model Definition
// Entity is the domain object base implementation that provides with an Id,
// key generator for id and some useful state methods.
// 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 sealed record 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 sealed record GetPersonRequest([Required] Guid Id) : IHttpClientRequest<Person>,
IHttpRequestPathString
{
public IDictionary<string, string> GetPathStringSource()
=> new Dictionary<string, string> { { nameof(Id), Id.ToString() } };
}
public sealed record 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 to behave like an expression
// when querying data, and override the target method.
public sealed class AddPersonCommand : QueryExpression<PersonEntity>, ICommand,
IValidatorDecorator
{
public Guid Id { get; }
public string FirstName { get; }
public string LastName { get; }
public override Expression<Func<PersonEntity>, bool>> GetExpression()
=> person => person.Id == Id;
}
public sealed class GetPersonQuery : QueryExpression<PersonEntity>, IQuery<Person>
{
public Guid Id { get; }
public GetPersonQuery(Guid id) => Id= id;
public override Expression<Func<PersonEntity>, bool>> GetExpression()
=> person => person.Id == Id;
}
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<IOperationResult> 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(
command.Id,
command.FirstName,
command.LastName);
await _savePerson(newPerson).configureAwait(false);
// The return result
return OperationResult
.Ok()
.AddHeaders("newId",newPerson.Id);
// or you can use the OperationResult.Created(...) response.
// Ok, NotFound... are extension methods that return an IOperationResult 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, ValueTask<PersonEntity?>> getPerson)
=> _getPerson = getPerson;
public override async ValueTask<IOperationResult<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.
// Validator{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, bool> _personExists;
public AddPersonCommandValidationDecorator(Func<Guid, bool> personExists)
=> _personExists = personExists;
public ValueTask<IOperationResult> Validate(AddPersonCommand argument)
{
return _personExists(argument.Id) switch
{
true => new(OperationResult.Conflict(nameof(argument.Id), "Id already exist")),
_ => new(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);
}
}
// Context 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
[Route("api/[controller]")]
[ApiController]
[AllowAnonymous]
public class PersonController : ControllerBase
{
private readonly IDispatcher _dispatcher;
public PersonController(IDispatcher dispatcher)
=> _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
[HttpPost]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ValidationProblemDetails))]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> AddPersonAsync(
[FromBody] AddPersonRequest request, CancellationToken cancellationToken = default)
{
var command = new AddPersonCommand(request.Id, request.FirstName, request.LastName);
IOperationResult result = await _dispatcher.SendAsync(command, cancellationToken)
.ConfigureAwait(false);
return Ok(result);
}
[HttpGet]
[Route("{id}")]
[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);
IOperationResult<Person> personResult = await _dispatcher.FetchAsync(query, cancellationToken)
.ConfigureAwait(false));
return Ok(personResult);
}
// ...
}
// 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)
{
services.AddOptions();
// common registrations...
services.AddXpandableServices()
.AddXOperationResultExceptionMiddleware()
.AddXOperationResultConfigureJsonOptions()
.AddXOperationResultConfigureMvcOptions()
.AddXDataContext<PersonContext>(Servicelifetime.Scoped, options => options.UseInMemoryDatabase(nameof(PersonContext))
.AddXDispatcher()
.AddXHandlerProvider()
.AddXHandlers(
options =>
{
options.UsePersistenceDecorator();
options.UseValidatorDecorator();
},
ServiceLifetime.Scoped,
typeof(AddPersonCommandHandler).Assembly)
.AddXPersistenceDecoratorDelegate(provider =>
{
var context = provider.GetRequiredService<PersonContext>();
return context.SaveChangesAsync;
})
.Build()
.AddControllers();
// 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 =>
provider.GetRequiredService<PersonContext>().AddAndSave<PersonEntity>);
// AddXpandableServices() will make available methods for registration
// AddXOperationResultExceptionMiddleware() handles the OperationResult exception
// AddXOperationResultConfigureJsonOptions() will add operation result converters
// AddXOperationResultConfigureMvcOptions() will add operation result filers
// 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)
{
...
app.UseXpandableApplications()
.UseXOperationResultExceptionMiddleware();
...
}
}
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.
[TestMethod]
[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(),
client);
var addPersonRequest = new AddPersonRequest(GuidGuid.NewGuid(), firstName, lastName);
using var response = await httpClientDispatcher.SendAsync(addPersonRequest).ConfigureAwait(false);
if (!response.IsValid())
{
Trace.WriteLine($"{response.StatusCode}");
IOperationResult operationResult = response.ToOperationResult();
// ToOperationResult() is an extension method for HttpRestClientResponse that returns
// an IOperationResult from the response.
foreach (OperationError error in operationResult.Errors)
{
Trace.WriteLine($"Key : {error.Key}");
Trace.WriteLine(error.ErrorMessages.StringJoin(";"));
}
}
else
{
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);
builder.RootComponents.Add<App>("#app");
builder.Services
.AddOptions()
.AddXpandableServices()
.AddXHttpClientDispatcher(httpClient =>
{
httpClient.BaseAddress = new Uri("https://localhost:44396"); // your api url
httpClient.DefaultRequestHeaders
.Accept
.Add(new MediaTypeWithQualityHeaderValue(ContentType.Json));
})
.Build();
// 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();
}
}
AddPerson.razor
<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>
<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>
<div class="form-group">
<div class="col-md-12 text-center">
<button class="col-md-12 btn btn-primary">
Add
</button>
</div>
</div>
</EditForm>
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.
AddPerson.razor.cs
public sealed class PersonModel
{
[Required]
public string FirstName { get; set; } = default!;
[Required]
public string LastName { get; set; } = default!;
}
public partial class AddPerson
{
protected XDataAnnotationsValidator Validator { get; set; } = default!;
[Inject]
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);
IOperationResult operationResult = addResponse.ToOperationResult();
Validator.ValidateModel(operationResult);
if (addResponse.IsValid()) // or operationResult.IsSuccess
{
// custom code like displaying the result
var createdId = operationResult.Headers["newId"];
}
}
}
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 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
{
....
services
.AddXpandableServices()
.AddXServiceExport(Configuration, options => options.SearchPattern = "your-search-pattern-dll")
.Build();
...
}
In the library you want types to be registered
[Export(typeof(IAddServiceExport))]
public sealed class RegisterServiceExport : IAddServiceExport
{
public void AddServices(IServiceCollection services, IConfiguration configuration)
{
services
.AddXpandableServices()
.AddXDispatcher()
.AddXTokenEngine<TokenEngine>()
.Build();
....
}
}
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
.AddXpandableServices()
.XTryDecorate<TService, TDecorator>()
.Build();
Suppose you want to add logging for the AddPersonCommand ...
// The AddPersonCommand decorator for logging
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<IOperationResult> HandleAsync(
AddPersonCommand command, CancellationToken cancellationToken = default)
{
_logger.Information(...);
var response = await _decoratee.HandleAsync(command, cancellationToken).configureAwait(false);
_logger.Information(...)
return response;
}
}
// Registration
services
.AddXpandableServices()
.XTryDecorate<AddPersonCommandHandler, AddPersonCommandHandlerLoggingDecorator>()
.Build();
// or
services.AddXpandableServices()
.AddXHandlers(
options =>
{
options.UseValidatorDecorator(); // this option will add the command decorator registration
},
typeof(AddPersonCommandHandler).Assembly)
.Build()
// 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<IOperationResult<TResult>> 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 class and implement ICommand{TResult} interface
services
.AddXpandableServices()
.XTryDecorate(typeof(ICommandHandler<>), typeof(CommandLoggingDecorator<>))
.Build();
Others
Libraries also provide with DDD model implementation using event sourcing and out-box pattern.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. 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. |
-
net6.0
- Microsoft.AspNetCore.Components.Web (>= 6.0.1)
- Xpandables.Net (>= 6.0.2)
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.1.1 | 872 | 8/6/2022 |
6.0.9 | 870 | 7/9/2022 |
6.0.8 | 843 | 6/27/2022 |
6.0.4 | 866 | 3/15/2022 |
6.0.3 | 893 | 2/22/2022 |
6.0.2 | 691 | 1/4/2022 |
6.0.1 | 707 | 12/4/2021 |
6.0.0 | 724 | 11/8/2021 |
6.0.0-rc.4.3 | 161 | 11/3/2021 |
6.0.0-rc.3.1 | 171 | 10/15/2021 |
6.0.0-rc.3 | 161 | 10/14/2021 |
6.0.0-rc.2 | 162 | 9/21/2021 |
6.0.0-preview.5 | 158 | 8/26/2021 |
5.6.1 | 799 | 6/30/2021 |
5.6.0 | 766 | 6/9/2021 |
5.5.1 | 754 | 5/26/2021 |
Breaking changes : Merge Xpandables.Net.DependencyInjection into Xpandables.Net