Xpandables.Net.DependencyInjection 6.0.1

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

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

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<CreatedId>, 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);
public sealed record CreatedId(Guid 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,
    IValidatorDecorator, IPersistenceDecorator
{
    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>
{
    private readonly PersonContext _personContext;
    public AddPersonCommandHandler(PersonContext personContext)
        => _personContext = personContext;
    
    public Task<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);
        
        _personContext.People.Add(newPerson);
        
        // You can make a call here to 
        // await _personContext.SaveChangeAsync(canellationToken).ConfigureAwait(false);
        // to persist changes or let the control flow to apply persistence because the command
        // AddPersonCommand is decorated with the IPersistenceDecorator interface.
        // In that case, you may register the persistence command using the
        // delegate PersistenceDecoratorDelegate.
        
        return Task.FromResult( OperationResult.OkOperation());
        
        // OkOperation, NotFoundOperation are extension 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 : IQueryHandler<GetPersonQuery, Person>
{
   private readonly PersonContext _personContext;
    public GetPersonQueryHandler(PersonContext personContext)
        => _personContext = personContext;    
    
    public override async Task<IOperationResult<Person>> HandleAsync(GetPersonQuery query,
        CancellationToken cancellationToken = default)
    {
        var result = await _personContext.People.FirstOrDefaultAsync(
            query,
            cancellationToken)
            .configureAwait(false);
        
        return result switch
        {
            PersonEntity person => OperationResult.OkOperation(new Person(person.Id, person.FirstName, person.LastName)),
            _ => OperationResult.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 PersonContext _personContext;
    public AddPersonCommandValidationDecorator(PersonContext personContext)
        => _personContext = personContext;
    
    public override IOperationResult Validate(AddPersonCommand argument)
    {
        Person? person = _personContext.People.FirstOrDefault(argument);

        return person switch
        {
            { } => OperationResult.BadOperation(nameof(argument.Id), "Id already exist"),
            null => OperationResult.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)

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, Type = typeof(CreatedId))]
    public async Task<IActionResult> AddPersonAsync(
        [FromBody] AddPersonRequest request, CancellationToken cancellationToken = default)
    {
        var command = new AddPersonCommand(request.Id, request.FirstName, request.LastName);
        await _dispatcher.SendAsync(command, cancellationToken).ConfigureAwait(false);
        return Created($"api/person/{command.Id}", new CreatdId(command.Id));
    }
    
    [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 sealed class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddOptions();
        
        // common registrations...
        
        services.AddXpandableServices()
            .AddXOperationResultExceptionMiddleware()
            .AddXOperationResultConfigureJsonOptions()
            .AddXOperationResultConfigureMvcOptions()
            .AddXDataContext<PersonContext>(options => options.UseInMemoryDatabase(nameof(PersonContext))
            .AddXDispatcher()
            .AddXHandlerProvider()
            .AddXHandlers(
                options =>
                {
                    options.UsePersistenceDecorator();
                    options.UseValidatorDecorator();
                },
                typeof(AddPersonCommandHandler).Assembly)
            .Build()
            .AddScoped<PersistenceDecoratorDelegate>(provider =>
            {
                var context = provider.GetRequiredService<PersonContext>();
                return context.SaveChangesAsync;
            })
            .AddControllers();

        // 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(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
    {
        CreatedId 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">

    <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<CreatedId>();
        Validator.ValidateModel(operationResult);

        if (addResponse.IsValid()) // or operationResult.IsSuccess
        {
            // custom code like displaying the result
            var createdId = operationResult.Value;
        }
    }    
}

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 Task<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 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 DDD model implementation using event sourcing and out-box pattern.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (5)

Showing the top 5 NuGet packages that depend on Xpandables.Net.DependencyInjection:

Package Downloads
Xpandables.Net.EntityFramework.DependencyInjection

A utility library in .Net5 that adds dependency injection to Xpandables.Net.EntityFramework

Xpandables.Net.HttpRestClient.DependencyInjection

A utility library in .Net5 that adds dependency injection to Xpandables.Net.HttpRestClient

Xpandables.Net.Extensibility.DependencyInjection

A utility library in .Net5 that adds dependency injection to Xpandables.Net.Extensibility

Xpandables.Net.QrCodes.DependencyInjection

A utility library in .Net5 that adds dependency injection to Xpandables.Net.QrCodes

Xpandables.Net.Data.DependencyInjection

A utility library in .Net5 that adds dependency injection to Xpandables.Net.Data

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
7.3.3 390 5/9/2023
7.1.4 444 2/26/2023
7.1.3 434 2/19/2023
7.0.0 555 11/9/2022
7.0.0-rc2.0.1 99 10/12/2022
7.0.0-rc1.0.0 137 9/26/2022
6.0.1 807 12/4/2021
6.0.0 893 11/8/2021
6.0.0-rc.4.3 146 11/3/2021
6.0.0-rc.3.1 153 10/15/2021
6.0.0-rc.3 149 10/14/2021
6.0.0-rc.2 157 9/21/2021
6.0.0-preview.5 156 8/26/2021
5.6.1 1,191 6/30/2021
5.6.0 1,176 6/9/2021
5.5.1 1,132 5/26/2021
5.4.4 416 4/12/2021
5.4.0 486 3/11/2021
5.3.14 426 3/2/2021
5.3.13 410 2/25/2021
5.3.12 449 2/21/2021
5.3.11 397 2/18/2021
5.3.10 394 2/18/2021
5.3.9 441 2/11/2021
5.3.8 466 2/10/2021
5.3.7 409 2/7/2021
5.3.5 437 2/7/2021
5.3.4 419 2/7/2021
5.3.3 417 2/5/2021
5.3.2 461 2/2/2021
5.3.1 415 1/31/2021
5.3.0 409 1/31/2021
5.2.14 465 1/26/2021
5.2.13 419 1/25/2021
5.2.12 452 1/22/2021
5.2.11 448 1/19/2021
5.2.10 421 1/16/2021
5.2.9 434 1/13/2021
5.2.8 449 1/8/2021
5.2.7 437 1/6/2021
5.2.6 412 1/6/2021
5.2.5 496 12/17/2020
5.2.4 452 12/12/2020
5.2.3 478 12/8/2020
5.2.2 454 12/7/2020
5.2.1 491 12/7/2020
5.2.0 531 12/6/2020
5.1.1 591 12/6/2020
5.1.0 458 12/5/2020
5.0.6 489 12/5/2020
5.0.5 473 11/23/2020
5.0.4 475 11/22/2020
5.0.3 600 11/20/2020
5.0.2 494 11/19/2020
5.0.1 521 11/16/2020
5.0.0 486 11/12/2020
5.0.0-rc.2.1.4 267 11/6/2020
5.0.0-rc.2.1.3 266 11/1/2020
5.0.0-rc.2.1.2 241 10/31/2020
5.0.0-rc.2.1.0 290 10/24/2020
5.0.0-rc.2.0.2 202 10/22/2020
5.0.0-rc.2.0.1 237 10/17/2020
5.0.0-rc.2.0.0 243 10/17/2020
5.0.0-rc.1.1.5 310 10/11/2020
5.0.0-rc.1.1.4 298 10/11/2020
5.0.0-rc.1.1.3 344 10/10/2020
5.0.0-rc.1.1.2 230 10/4/2020
5.0.0-rc.1.1.1 247 10/2/2020
5.0.0-rc.1.1.0 220 10/1/2020
5.0.0-rc.1.0.9 285 9/29/2020
5.0.0-rc.1.0.8 207 9/28/2020
5.0.0-rc.1.0.7 248 9/28/2020
5.0.0-rc.1.0.6 312 9/26/2020
5.0.0-rc.1.0.5 262 9/25/2020
5.0.0-rc.1.0.4 225 9/24/2020
5.0.0-rc.1.0.3 284 9/23/2020
5.0.0-rc.1.0.2 242 9/23/2020
5.0.0-rc.1.0.1 225 9/21/2020
5.0.0-preview.2.0.0 243 8/16/2020
5.0.0-preview.1.0.8 231 8/10/2020
5.0.0-preview.1.0.7 286 8/10/2020
5.0.0-preview.1.0.6 292 8/5/2020
3.2.1 496 9/17/2020
3.2.0 548 9/15/2020
3.1.9 534 9/13/2020
3.1.8 579 9/13/2020
3.1.7 517 9/12/2020
3.1.6 677 9/5/2020
3.1.5 523 9/4/2020
3.1.4 579 8/29/2020
3.1.3 489 8/28/2020
3.1.2 488 8/27/2020
3.1.1 506 8/24/2020
3.1.0 564 8/19/2020

Fix registration