Xpandables.Net.AspNetCore
6.0.0-rc.3.1
See the version list below for details.
dotnet add package Xpandables.Net.AspNetCore --version 6.0.0-rc.3.1
NuGet\Install-Package Xpandables.Net.AspNetCore -Version 6.0.0-rc.3.1
<PackageReference Include="Xpandables.Net.AspNetCore" Version="6.0.0-rc.3.1" />
paket add Xpandables.Net.AspNetCore --version 6.0.0-rc.3.1
#r "nuget: Xpandables.Net.AspNetCore, 6.0.0-rc.3.1"
// Install Xpandables.Net.AspNetCore as a Cake Addin #addin nuget:?package=Xpandables.Net.AspNetCore&version=6.0.0-rc.3.1&prerelease // Install Xpandables.Net.AspNetCore as a Cake Tool #tool nuget:?package=Xpandables.Net.AspNetCore&version=6.0.0-rc.3.1&prerelease
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(string firstName, string lastName)
{
// custom check
return new(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(string firstName, string lastName)
=> (FirstName, Lastname) = (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 = HttpMethodVerbs.Post, IsSecured = false)]
public sealed record AddPersonRequest([Required] string FirstName, [Required] string LastName)
: IHttpClientRequest<CreatedId>, IHttpRequestString
{
// You can omit the use of IHttpRequestString, the whole class will be serialised.
public object GetStringContent() => new { FirstName, LastName };
}
[HttpClient(Path = "api/person/{id}", IsNullable = true, In = ParameterLocation.Path,
Method = HttpMethodVerbs.Get, IsSecured = false)]
public sealed record GetPersonRequest([Required] string Id) : IHttpClientRequest<Person>,
IHttpRequestPathString
{
public IDictionary<string, string> GetPathStringSource()
=> new Dictionary<string, string> { { nameof(Id), Id } };
}
public sealed record Person(string FirstName, string LastName);
public sealed record CreatedId(string Id);
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<CreatedId>,
IValidatorDecorator, IPersistenceDecorator
{
public string FirstName { get; }
public string LastName { get; }
public override Expression<Func<PersonEntity>, bool>> GetExpression()
=> person => person.FistName == FistName
&& person.LastName == LastName;
}
public sealed class GetPersonQuery : QueryExpression<PersonEntity>, IQuery<Person>
{
public string Id { get; }
public GetPersonQuery(string id) => Id= id;
public override Expression<Func<PersonEntity>, bool>> GetExpression()
=> person => person.Id == Id;
}
// CommandHandler{TCommand}, CommandHandler{TCommand, TResult} and QueryHandler{TResult} are abstract classes
// that implement ICommandHandler{TCommand}, ICommandHandler{TCommand, TResult} and IQueryHandler{TResult}.
public sealed class AddPersonCommandHandler : CommandHandler<AddPersonCommand, CreatedId>
{
private readonly IPersonUnitOfWork _personUnitOfWork;
public AddPersonCommandHandler(IPersonUnitOfWork personUnitOfWork)
=> _personUnitOfWork = personUnitOfWork;
public override async Task<IOperationResult<CreatedId>> 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.FirstName,
command.LastName);
await _personUnitOfWork.People.InsertAsync(newPerson, cancellationToken).configureAwait(false);
// You can make a call here to
// await _personUnitOfWork.PersistAsync(canellationToken).ConfigureAwait(false);
// to persist changes or let the control flow to apply persistence because the command
// AddPersonCommand is decorated with the IPersistenceDecorator interface.
return this.OkOperation(new CreatedId(newPerson.Id));
// OkOperation NotFoundOperation are extenion methods that return an IOperationResult that acts like
// IActionResult in AspNetCore.
// Note that data will be saved at the end of the control flow
// if there is no error from other process. The OperationResultFilter will process the output message format.
// You can add a decorator class to manage the exception.
}
}
public sealed class GetPersonQueryHandler : QueryHandler<GetPersonQuery, Person>
{
private readonly IPersonUnitOfWork _personUnitOfWork;
public GetPersonQueryHandler(IPersonUnitOfWork personUnitOfWork)
=> _personUnitOfWork = personUnitOfWork;
public override async Task<IOperationResult<Person>> HandleAsync(GetPersonQuery query,
CancellationToken cancellationToken = default)
{
var result = await _personUnitOfWork.People.TryFindAsync(
query,
cancellationToken)
.configureAwait(false);
return result switch
{
{ } person => this.OkOperation(new Person(person.FirstName, person.LastName)),
_ => this.NotFoundOperation<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, 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 : Validator<AddPersonCommand>
{
private ready IPersonUnitOfWork _personUnitOfWork;
public AddPersonCommandValidationDecorator(IPersonUnitOfWork personUnitOfWork)
=> _personUnitOfWork = personUnitOfWork;
public override async Task<IOperationResult> ValidateAsync(AddPersonCommand argument,
CancellationToken cancellationToken)
{
return await _personUnitOfWork.People.TryFindAsync(argument, cancellationToken).configureAwait(false) switch
{
{ } => this.BadOperation(nameof(argument.FirstName), "Already exist"),
null => this.OkOperation()
};
}
}
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 implements the IUnitOfWorkContext interface.
// IUnitOfWorkContext interface represents a marker interface for classes related to unit of work.
public sealed class PersonContext : Context
{
public PersonContext(DbContextOptions<PersonContext> contextOptions)
: base(contextOptions) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new PersonEntityTypeConfiguration());
}
public DbSet<PersonEntity> People { get; set; } = default!;
}
// The unit of Work interface (The name is not important for the case)
// You can directly use the IUnitOfWork interface and its implementation UnitOfWork, and in that case,
// you may use _unitOfWork.GetRepository<PersonEntity>()... where _unitOfWork is the reference to the instance.
// IUnitOfWork, IRepository<>, IUnitOfWorkContextFactory and implementations are provided by the library.
public interface IPersonUnitOfWork : IUnitOfWork
{
IRepository<PersonEntity> People { get; }
}
public sealed class PersonUnitOfWork : UnitOfWork<PersonContext>, IPersonUnitOfWork
{
public PersonUnitOfWork(IUnitOfWorkContextFactory unitOfWorkContextFactory) : base(unitOfWorkContextFactory)
{
People = new Repository<PersonEntity>(Context);
}
public IRepository<PersonEntity> People { get; }
}
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.Status200OK, Type = typeof(CreatedId))]
public async Task<IActionResult> AddPersonAsync(
[FromBody] AddPersonRequest request, CancellationToken cancellationToken = default)
{
var command = new AddPersonCommand(request.FirstName, request.LastName);
return Ok(await _dispatcher.SendAsync(command, cancellationToken).ConfigureAwait(false));
}
[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);
return Ok(await _dispatcher.FetchAsync(query, cancellationToken).ConfigureAwait(false));
}
// ...
}
// The startup class
// We will register handlers, context, validators and decorators.
public sealed class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddOptions();
// common registrations...
services.AddTransient<OperationResultExceptionMiddleware>();
services.AddXpandableServices()
.AddXOperationResultConfigureJsonOptions()
.AddXOperationResultConfigureMvcOptions()
.AddXDataContext<PersonContext>(options => options.UseInMemoryDatabase(nameof(PersonContext))
.AddXDispatcher()
.AddXHandlerAccessor()
.AddXUnitOfWork<IPersonUnitOfWork, PersonUnitOfWork>()
// or you may use AddXUnitOfWorkContext<PersonContext>() for directly use of IUnitOfWork
.AddXHandlers(
options =>
{
options.UsePersistenceDecorator();
options.UseValidatorDecorator();
},
new[] { typeof(AddPersonCommandHandler).Assembly },
)
.Build()
.AddControllers();
// services.AddTransient<OperationResultExceptionMiddleware>() handles the operationresult exception
// AddXpandableServices() will make available methods for registration
// AddXOperationResultConfigureJsonOptions() will add operation result converters
// AddXOperationResultConfigureMvcOptions() will add operation result filers
// AddXDataContext{TContext} registers the TContext and make it available for IPersonUnitOfWork as IUnitOfWorkContext
// AddXDispatcher() registers the dispatcher
// AddXHandlerAccessor() registers a handlers accessor using the IHandlerAccessor interface
// AddXUnitOfWork() register the IPersonUnitOfWork interface.
// AddXHandlers(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 your api 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(firstName, lastName);
using var response = await httpClientDispatcher.SendAsync(addPersonRequest).ConfigureAwait(false);
if (!response.IsValid())
{
Trace.WriteLine($"{response.StatusCode}");
var errors = response.GetBadOperationResult().Errors;
// GetBadOperationResult() is an extension method for HttpRestClientResponse that returns
// a failed IOperationResult from the response.
foreach (var error in errors)
{
Trace.WriteLine($"Key : {error.Key}");
Trace.WriteLine(error.ErrorMessages.StringJoin(";"));
}
}
else
{
var createdId = response.Result;
Trace.WriteLine($"Added person : {createdId.Id}");
}
}
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">
<DataAnnotationsValidatorExtended @ref="@Validator" />
<div class="form-group">
<label for="FirstName" class="-form-label">First Name</label>
<InputTextOnInput @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>
<InputTextOnInput @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>
InputTextOnInput is a component that allows text to be validated on input.
DataAnnotationsValidatorExtended 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 DataAnnotationsValidatorExtended 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(model.FirstName, model.LastName);
using var addResponse = await HttpClientDispatcher.SendAsync(addRrequest).ConfigureAwait(false);
if (!addResponse.IsValid())
{
var operationResult = addResponse.GetBadOperationResult();
Validator.ValidateModel(operationResult);
}
else
{
// custom code like displaying the result
var createdId = addResponse.Result;
}
StateHasChanged();
}
}
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, CreatedId>
{
private readonly ICommandHandler<AddPersonCommand, CreatedId> _ decoratee;
private readonly ILogger<AddPersonCommandHandler> _logger;
public AddPersonCommandHandlerLoggingDecorator(
ILogger<AddPersonCommandHandler> logger,
ICommandHandler<AddPersonCommand, CreatedId> decoratee)
=> (_logger, _ decoratee) = (logger, decoratee);
public async Task<IOperationResult<CreatedId>> 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 you can define the generic model, for all commands that implement ICommand
// interface or something else.
public sealed class CommandLoggingDecorator<TCommand, TResult> : ICommandHandler<TCommand, TResult>
where TCommand : class, ICommand<TResult> // you can add more constraints
{
private readonly ICommandHandler<TCommand, TResult> _ decoratee;
private readonly ILogger<TCommand> _logger;
public CommandLoggingDecorator(ILogger<TCommand> logger, ICommandHandler<TCommand, TResult> decoratee)
=> (_logger, _ decoratee) = (logger, decoratee);
public async Task<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 Aggregate 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
- Xpandables.Net.DependencyInjection (>= 6.0.0-rc.3.1)
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 |
---|---|---|
9.0.0-rc.1 | 43 | 10/26/2024 |
8.1.2 | 112 | 9/12/2024 |
8.0.8 | 110 | 6/21/2024 |
8.0.6 | 96 | 5/25/2024 |
8.0.5 | 105 | 5/18/2024 |
8.0.1 | 297 | 2/11/2024 |
8.0.0 | 439 | 12/3/2023 |
8.0.0-rc.2.1.1 | 92 | 11/12/2023 |
8.0.0-rc.2.1 | 77 | 11/6/2023 |
8.0.0-rc.2.0 | 71 | 11/5/2023 |
7.3.3 | 492 | 5/9/2023 |
7.1.4 | 559 | 2/26/2023 |
7.1.3 | 603 | 2/19/2023 |
7.0.0 | 661 | 11/9/2022 |
7.0.0-rc2.0.1 | 100 | 10/12/2022 |
7.0.0-rc1.0.0 | 144 | 9/26/2022 |
6.1.1 | 744 | 8/6/2022 |
6.0.9 | 752 | 7/9/2022 |
6.0.8 | 779 | 6/27/2022 |
6.0.4 | 797 | 3/15/2022 |
6.0.3 | 724 | 2/22/2022 |
6.0.2 | 566 | 1/4/2022 |
6.0.1 | 556 | 12/4/2021 |
6.0.0 | 616 | 11/8/2021 |
6.0.0-rc.4.3 | 150 | 11/3/2021 |
6.0.0-rc.3.1 | 163 | 10/15/2021 |
6.0.0-rc.3 | 150 | 10/14/2021 |
6.0.0-rc.2 | 155 | 9/21/2021 |
6.0.0-preview.5 | 167 | 8/26/2021 |
5.6.1 | 666 | 6/30/2021 |
5.6.0 | 671 | 6/9/2021 |
5.5.1 | 631 | 5/26/2021 |
5.4.4 | 631 | 4/12/2021 |
update to RC2