Mediator.Abstractions 3.0.0-preview.27

This is a prerelease version of Mediator.Abstractions.
dotnet add package Mediator.Abstractions --version 3.0.0-preview.27                
NuGet\Install-Package Mediator.Abstractions -Version 3.0.0-preview.27                
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="Mediator.Abstractions" Version="3.0.0-preview.27" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Mediator.Abstractions --version 3.0.0-preview.27                
#r "nuget: Mediator.Abstractions, 3.0.0-preview.27"                
#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 Mediator.Abstractions as a Cake Addin
#addin nuget:?package=Mediator.Abstractions&version=3.0.0-preview.27&prerelease

// Install Mediator.Abstractions as a Cake Tool
#tool nuget:?package=Mediator.Abstractions&version=3.0.0-preview.27&prerelease                

GitHub Workflow Status GitHub Downloads<br/> Abstractions NuGet current SourceGenerator NuGet current Abstractions NuGet prerelease SourceGenerator NuGet prerelease<br/>

Mediator

Note

Version 3.0 is currently being developed. See status and provide feedback here (#98)

This is a high performance .NET implementation of the Mediator pattern using the source generators feature introduced in .NET 5. The API and usage is mostly based on the great MediatR library, with some deviations to allow for better performance. Packages are .NET Standard 2.1 compatible.

The mediator pattern is great for implementing cross cutting concern (logging, metrics, etc) and avoiding "fat" constructors due to lots of injected services.

Goals for this library

  • High performance
    • Runtime performance can be the same for both runtime reflection and source generator based approaches, but it's easier to optimize in the latter case
  • AOT friendly
    • MS are investing time in various AOT scenarios, and for example iOS requires AOT compilation
  • Build time errors instead of runtime errors
    • The generator includes diagnostics, i.e. if a handler is not defined for a request, a warning is emitted

In particular, source generators in this library is used to

  • Generate code for DI registration
  • Generate code for IMediator implementation
    • Request/Command/Query Send methods are monomorphized (1 method per T), the generic ISender.Send methods rely on these
    • You can use both IMediator and Mediator, the latter allows for better performance
  • Generate diagnostics related messages and message handlers

See this great video by @Elfocrash / Nick Chapsas, covering both similarities and differences between Mediator and MediatR

Using MediatR in .NET? Maybe replace it with this

Table of Contents

2. Benchmarks

This benchmark exposes the perf overhead of the libraries. Mediator (this library) and MediatR methods show the overhead of the respective mediator implementations. I've also included the MessagePipe library as it also has great performance.

  • <SendRequest | Stream>_Baseline: simple method call into the handler class
  • <SendRequest | Stream>_Mediator: the concrete Mediator class generated by this library
  • <SendRequest | Stream>_MessagePipe: the MessagePipe library
  • <SendRequest | Stream>_IMediator: call through the IMediator interface in this library
  • <SendRequest | Stream>_MediatR: the MediatR library

See benchmarks code for more details on the measurement.

Warning

A current limitation of this library is that performance degrades significantly for projects with a large number of messages (>500) There is ongoing work on resolving this for version 3.0 (#48).

Requests benchmark

Stream benchmark

3. Usage and abstractions

There are two NuGet packages needed to use this library

  • Mediator.SourceGenerator
    • To generate the IMediator implementation and dependency injection setup.
  • Mediator.Abstractions
    • Message types (IRequest<,>, INotification), handler types (IRequestHandler<,>, INotificationHandler<>), pipeline types (IPipelineBehavior)

You install the source generator package into your edge/outermost project (i.e. ASP.NET Core application, Background worker project), and then use the Mediator.Abstractions package wherever you define message types and handlers. Standard message handlers are automatically picked up and added to the DI container in the generated AddMediator method. Pipeline behaviors need to be added manually (including pre/post/exception behaviors).

For example implementations, see the /samples folder. See the ASP.NET Core clean architecture sample for a more real world setup.

3.1. Message types

  • IMessage - marker interface
  • IStreamMessage - marker interface
  • IBaseRequest - marker interface for requests
  • IRequest - a request message, no return value (ValueTask<Unit>)
  • IRequest<out TResponse> - a request message with a response (ValueTask<TResponse>)
  • IStreamRequest<out TResponse> - a request message with a streaming response (IAsyncEnumerable<TResponse>)
  • IBaseCommand - marker interface for commands
  • ICommand - a command message, no return value (ValueTask<Unit>)
  • ICommand<out TResponse> - a command message with a response (ValueTask<TResponse>)
  • IStreamCommand<out TResponse> - a command message with a streaming response (IAsyncEnumerable<TResponse>)
  • IBaseQuery - marker interface for queries
  • IQuery<out TResponse> - a query message with a response (ValueTask<TResponse>)
  • IStreamQuery<out TResponse> - a query message with a streaming response (IAsyncEnumerable<TResponse>)
  • INotification - a notification message, no return value (ValueTask)

As you can see, you can achieve the exact same thing with requests, commands and queries. But I find the distinction in naming useful if you for example use the CQRS pattern or for some reason have a preference on naming in your application.

3.2. Handler types

  • IRequestHandler<in TRequest>
  • IRequestHandler<in TRequest, TResponse>
  • IStreamRequestHandler<in TRequest, out TResponse>
  • ICommandHandler<in TCommand>
  • ICommandHandler<in TCommand, TResponse>
  • IStreamCommandHandler<in TCommand, out TResponse>
  • IQueryHandler<in TQuery, TResponse>
  • IStreamQueryHandler<in TQuery, out TResponse>
  • INotificationHandler<in TNotification>

These types are used in correlation with the message types above.

3.3. Pipeline types

  • IPipelineBehavior<TMessage, TResponse>
  • IStreamPipelineBehavior<TMessage, TResponse>
  • MessagePreProcessor<TMessage, TResponse>
  • MessagePostProcessor<TMessage, TResponse>
  • MessageExceptionHandler<TMessage, TResponse, TException>
3.3.1. Message validation example
// As a normal pipeline behavior
public sealed class MessageValidatorBehaviour<TMessage, TResponse> : IPipelineBehavior<TMessage, TResponse>
    where TMessage : IValidate
{
    public ValueTask<TResponse> Handle(
        TMessage message,
        CancellationToken cancellationToken,
        MessageHandlerDelegate<TMessage, TResponse> next
    )
    {
        if (!message.IsValid(out var validationError))
            throw new ValidationException(validationError);

        return next(message, cancellationToken);
    }
}

// Or as a pre-processor
public sealed class MessageValidatorBehaviour<TMessage, TResponse> : MessagePreProcessor<TMessage, TResponse>
    where TMessage : IValidate
{
    protected override ValueTask Handle(TMessage message, CancellationToken cancellationToken)
    {
        if (!message.IsValid(out var validationError))
            throw new ValidationException(validationError);

        return default;
    }
}

// Register as IPipelineBehavior<,> in either case
services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(MessageValidatorBehaviour<,>))
3.3.2. Error logging example
// As a normal pipeline behavior
public sealed class ErrorLoggingBehaviour<TMessage, TResponse> : IPipelineBehavior<TMessage, TResponse>
    where TMessage : IMessage
{
    private readonly ILogger<ErrorLoggingBehaviour<TMessage, TResponse>> _logger;

    public ErrorLoggingBehaviour(ILogger<ErrorLoggingBehaviour<TMessage, TResponse>> logger)
    {
        _logger = logger;
    }

    public async ValueTask<TResponse> Handle(
        TMessage message,
        CancellationToken cancellationToken,
        MessageHandlerDelegate<TMessage, TResponse> next
    )
    {
        try
        {
            return await next(message, cancellationToken);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error handling message of type {messageType}", message.GetType().Name);
            throw;
        }
    }
}

// Or as an exception handler
public sealed class ErrorLoggingBehaviour<TMessage, TResponse> : MessageExceptionHandler<TMessage, TResponse>
    where TMessage : notnull, IMessage
{
    private readonly ILogger<ErrorLoggingBehaviour<TMessage, TResponse>> _logger;

    public ErrorLoggingBehaviour(ILogger<ErrorLoggingBehaviour<TMessage, TResponse>> logger)
    {
        _logger = logger;
    }

    protected override ValueTask<ExceptionHandlingResult<TResponse>> Handle(
        TMessage message,
        Exception exception,
        CancellationToken cancellationToken
    )
    {
        _logger.LogError(exception, "Error handling message of type {messageType}", message.GetType().Name);
        // Let the exception bubble up by using the base class helper NotHandled:
        return NotHandled;
        // Or if the exception is properly handled, you can just return your own response,
        // using the base class helper Handle().
        // This requires you to know something about TResponse,
        // so TResponse needs to be constrained to something,
        // typically with a static abstract member acting as a consructor on an interface or abstract class.
        return Handled(null!);
    }
}

// Register as IPipelineBehavior<,> in either case
services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(ErrorLoggingBehaviour<,>))

3.4. Configuration

There are two ways to configure Mediator. Configuration values are needed during compile-time since this is a source generator:

  • Assembly level attribute for configuration: MediatorOptionsAttribute
  • Options configuration delegate in AddMediator function.
services.AddMediator(options =>
{
    options.Namespace = "SimpleConsole.Mediator";
    options.DefaultServiceLifetime = ServiceLifetime.Transient;
});

// or

[assembly: MediatorOptions(Namespace = "SimpleConsole.Mediator", DefaultServiceLifetime = ServiceLifetime.Transient)]
  • Namespace - where the IMediator implementation is generated
  • DefaultServiceLifetime - the DI service lifetime
    • Singleton - (default value) everything registered as singletons, minimal allocations
    • Transient - mediator and handlers registered as transient
    • Scoped - mediator and handlers registered as scoped

4. Getting started

In this section we will get started with Mediator and go through a sample illustrating the various ways the Mediator pattern can be used in an application.

See the full runnable sample code in the Showcase sample.

4.1. Add package

dotnet add package Mediator.SourceGenerator --version 2.0.*
dotnet add package Mediator.Abstractions --version 2.0.*

or

<PackageReference Include="Mediator.SourceGenerator" Version="2.0.*">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Mediator.Abstractions" Version="2.0.*" />

4.2. Add Mediator to DI container

In ConfigureServices or equivalent, call AddMediator (unless MediatorOptions is configured, default namespace is Mediator). This registers your handler below.

using Mediator;
using Microsoft.Extensions.DependencyInjection;
using System;

var services = new ServiceCollection(); // Most likely IServiceCollection comes from IHostBuilder/Generic host abstraction in Microsoft.Extensions.Hosting

services.AddMediator();
var serviceProvider = services.BuildServiceProvider();

4.3. Create IRequest<> type

var mediator = serviceProvider.GetRequiredService<IMediator>();
var ping = new Ping(Guid.NewGuid());
var pong = await mediator.Send(ping);
Debug.Assert(ping.Id == pong.Id);

// ...

public sealed record Ping(Guid Id) : IRequest<Pong>;

public sealed record Pong(Guid Id);

public sealed class PingHandler : IRequestHandler<Ping, Pong>
{
    public ValueTask<Pong> Handle(Ping request, CancellationToken cancellationToken)
    {
        return new ValueTask<Pong>(new Pong(request.Id));
    }
}

As soon as you code up message types, the source generator will add DI registrations automatically (inside AddMediator). P.S - You can inspect the code yourself - open Mediator.g.cs in VS from Project → Dependencies → Analyzers → Mediator.SourceGenerator → Mediator.SourceGenerator.MediatorGenerator, or just F12 through the code.

4.4. Use pipeline behaviors

The pipeline behavior below validates all incoming Ping messages. Pipeline behaviors currently must be added manually.

services.AddMediator();
services.AddSingleton<IPipelineBehavior<Ping, Pong>, PingValidator>();

public sealed class PingValidator : IPipelineBehavior<Ping, Pong>
{
    public ValueTask<Pong> Handle(Ping request, MessageHandlerDelegate<Ping, Pong> next, CancellationToken cancellationToken)
    {
        if (request is null || request.Id == default)
            throw new ArgumentException("Invalid input");

        return next(request, cancellationToken);
    }
}

4.5. Constrain IPipelineBehavior<,> message with open generics

Add open generic handler to process all or a subset of messages passing through Mediator. This handler will log any error that is thrown from message handlers (IRequest, ICommand, IQuery). It also publishes a notification allowing notification handlers to react to errors. Message pre- and post-processors along with the exception handlers can also constrain the generic type parameters in the same way.

services.AddMediator();
services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(ErrorLoggerHandler<,>));

public sealed record ErrorMessage(Exception Exception) : INotification;
public sealed record SuccessfulMessage() : INotification;

public sealed class ErrorLoggerHandler<TMessage, TResponse> : IPipelineBehavior<TMessage, TResponse>
    where TMessage : IMessage // Constrained to IMessage, or constrain to IBaseCommand or any custom interface you've implemented
{
    private readonly ILogger<ErrorLoggerHandler<TMessage, TResponse>> _logger;
    private readonly IMediator _mediator;

    public ErrorLoggerHandler(ILogger<ErrorLoggerHandler<TMessage, TResponse>> logger, IMediator mediator)
    {
        _logger = logger;
        _mediator = mediator;
    }

    public async ValueTask<TResponse> Handle(TMessage message, MessageHandlerDelegate<TMessage, TResponse> next, CancellationToken cancellationToken)
    {
        try
        {
            var response = await next(message, cancellationToken);
            return response;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error handling message");
            await _mediator.Publish(new ErrorMessage(ex));
            throw;
        }
    }
}

4.6. Use notifications

We can define a notification handler to catch errors from the above pipeline behavior.

// Notification handlers are automatically added to DI container

public sealed class ErrorNotificationHandler : INotificationHandler<ErrorMessage>
{
    public ValueTask Handle(ErrorMessage error, CancellationToken cancellationToken)
    {
        // Could log to application insights or something...
        return default;
    }
}

4.7. Polymorphic dispatch with notification handlers

We can also define a notification handler that receives all notifications.


public sealed class StatsNotificationHandler : INotificationHandler<INotification> // or any other interface deriving from INotification
{
    private long _messageCount;
    private long _messageErrorCount;

    public (long MessageCount, long MessageErrorCount) Stats => (_messageCount, _messageErrorCount);

    public ValueTask Handle(INotification notification, CancellationToken cancellationToken)
    {
        Interlocked.Increment(ref _messageCount);
        if (notification is ErrorMessage)
            Interlocked.Increment(ref _messageErrorCount);
        return default;
    }
}

4.8. Notification handlers also support open generics

public sealed class GenericNotificationHandler<TNotification> : INotificationHandler<TNotification>
    where TNotification : INotification // Generic notification handlers will be registered as open constrained types automatically
{
    public ValueTask Handle(TNotification notification, CancellationToken cancellationToken)
    {
        return default;
    }
}

4.9. Notification publishers

Notification publishers are responsible for dispatching notifications to a collection of handlers. There are two built in implementations:

  • ForeachAwaitPublisher - the default, dispatches the notifications to handlers in order 1-by-1
  • TaskWhenAllPublisher - dispatches notifications in parallel

Both of these try to be efficient by handling a number of special cases (early exit on sync completion, single-handler, array of handlers). Below we implement a custom one by simply using Task.WhenAll.

services.AddMediator(options =>
{
    options.NotificationPublisherType = typeof(FireAndForgetNotificationPublisher);
});

public sealed class FireAndForgetNotificationPublisher : INotificationPublisher
{
    public async ValueTask Publish<TNotification>(
        NotificationHandlers<TNotification> handlers,
        TNotification notification,
        CancellationToken cancellationToken
    )
        where TNotification : INotification
    {
        try
        {
            await Task.WhenAll(handlers.Select(handler => handler.Handle(notification, cancellationToken).AsTask()));
        }
        catch (Exception ex)
        {
            // Notifications should be fire-and-forget, we just need to log it!
            // This way we don't have to worry about exceptions bubbling up when publishing notifications
            Console.Error.WriteLine(ex);

            // NOTE: not necessarily saying this is a good idea!
        }
    }
}

4.10. Use streaming messages

Since version 1.* of this library there is support for streaming using IAsyncEnumerable.

var mediator = serviceProvider.GetRequiredService<IMediator>();

var ping = new StreamPing(Guid.NewGuid());

await foreach (var pong in mediator.CreateStream(ping))
{
    Debug.Assert(ping.Id == pong.Id);
    Console.WriteLine("Received pong!"); // Should log 5 times
}

// ...

public sealed record StreamPing(Guid Id) : IStreamRequest<Pong>;

public sealed record Pong(Guid Id);

public sealed class PingHandler : IStreamRequestHandler<StreamPing, Pong>
{
    public async IAsyncEnumerable<Pong> Handle(StreamPing request, [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        for (int i = 0; i < 5; i++)
        {
            await Task.Delay(1000, cancellationToken);
            yield return new Pong(request.Id);
        }
    }
}

5. Diagnostics

Since this is a source generator, diagnostics are also included. Examples below

  • Missing request handler

Missing request handler

  • Multiple request handlers found

Multiple request handlers found

6. Differences from MediatR

This is a work in progress list on the differences between this library and MediatR.

  • RequestHandlerDelegate<TResponse>()MessageHandlerDelegate<TMessage, TResponse>(TMessage message, CancellationToken cancellationToken)
    • This is to avoid excessive closure allocations. I think it's worthwhile when the cost is simply passing along the message and the cancellation token.
  • No ServiceFactory
    • This library relies on the Microsoft.Extensions.DependencyInjection, so it only works with DI containers that integrate with those abstractions.
  • Singleton service lifetime by default
    • MediatR in combination with MediatR.Extensions.Microsoft.DependencyInjection does transient service registration by default, which leads to a lot of allocations. Even if it is configured for singleton lifetime, IMediator and ServiceFactory services are registered as transient (not configurable).
  • Methods return ValueTask<T> instead of Task<T>, to allow for fewer allocations (for example if the handler completes synchronously, or using async method builder pooling/PoolingAsyncValueTaskMethodBuilder<T>)
  • This library doesn't support generic requests/notifications

7. Versioning

For versioning this library I try to follow semver 2.0 as best as I can, meaning

  • Major bump for breaking changes
  • Minor bump for new backward compatible features
  • Patch bump for bugfixes
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (19)

Showing the top 5 NuGet packages that depend on Mediator.Abstractions:

Package Downloads
Pina.Cqrs

Package Description

Core-State

A fork of the Core state management library by TimeWarp for use in .Net Maui

Eternet.Mediator

Eternet Mediator extensions

Eternet.Mediator.Abstractions

Eternet Mediator Abstractions

Foresail.Core.Mediator.Abstraction

Foresail.Core Mediator abstraction components

GitHub repositories (3)

Showing the top 3 popular GitHub repositories that depend on Mediator.Abstractions:

Repository Stars
babaktaremi/Clean-Architecture-Template
🧹 Ready to develop template based on clean architecture principles. Supports ASP NET Core Identity integrated with JWE tokens, OTP authentication, stand alone plugin development, CQRS pattern using MediatR library and dynamic permission management system out of the box
josephnhtam/live-streaming-server-net
A .NET implementation of RTMP live streaming server, supporting HTTP-FLV, WebSocket-FLV, HLS, Kubernetes, cloud storage services integration and more.
ktutak1337/Stellar-Chat
A versatile multi-modal chat application that enables users to develop custom agents, create images, leverage visual recognition, and engage in voice interactions. It integrates seamlessly with local LLMs and commercial models like OpenAI, Gemini, Perplexity, and Claude, and allows to converse with uploaded documents and websites.
Version Downloads Last updated
3.0.0-preview.27 43,979 4/8/2024
3.0.0-preview.26 75 4/8/2024
3.0.0-preview.25 96 4/7/2024
3.0.0-preview.24 8,204 3/24/2024
3.0.0-preview.23 63 3/24/2024
3.0.0-preview.22 62 3/24/2024
3.0.0-preview.21 3,864 3/3/2024
3.0.0-preview.20 63 3/3/2024
3.0.0-preview.19 2,836 2/20/2024
3.0.0-preview.18 69 2/20/2024
3.0.0-preview.17 76 2/20/2024
3.0.0-preview.15 21,400 12/14/2023
3.0.0-preview.13 1,761 12/7/2023
3.0.0-preview.12 86 12/7/2023
3.0.0-preview.11 12,896 9/21/2023
3.0.0-preview.10 1,122 9/14/2023
3.0.0-preview.9 116 9/14/2023
3.0.0-preview.8 1,060 8/22/2023
3.0.0-preview.6 7,801 6/22/2023
3.0.0-preview.5 114 6/21/2023
3.0.0-preview.4 96 6/21/2023
3.0.0-preview.3 94 6/21/2023
3.0.0-preview.1 99 6/21/2023
2.2.0-preview.7 347 6/20/2023
2.2.0-preview.6 98 6/20/2023
2.2.0-preview.5 294 6/13/2023
2.2.0-preview.3 116 6/13/2023
2.2.0-preview.2 10,307 3/1/2023
2.1.7 742,471 9/21/2023
2.1.6 6,827 9/14/2023
2.1.5 141,300 6/20/2023
2.1.3 6,335 6/13/2023
2.1.2 1,711 6/13/2023
2.1.1 105,737 3/1/2023
2.1.0-preview.13 107 3/1/2023
2.1.0-preview.12 109 3/1/2023
2.1.0-preview.11 948 2/10/2023
2.1.0-preview.10 3,611 1/15/2023
2.1.0-preview.9 127 1/14/2023
2.1.0-preview.8 300 1/14/2023
2.1.0-preview.7 2,063 1/10/2023
2.1.0-preview.6 126 1/10/2023
2.1.0-preview.5 4,579 12/4/2022
2.1.0-preview.4 103 12/4/2022
2.1.0-preview.3 214 12/4/2022
2.1.0-preview.2 6,459 11/17/2022
2.1.0-preview.1 114 11/17/2022
2.0.30 90,004 1/10/2023
2.0.29 30,453 12/4/2022
2.0.28-rc 288 11/17/2022
2.0.27-rc 6,177 8/14/2022
2.0.26-preview 1,362 8/11/2022
2.0.25-preview 183 8/11/2022
2.0.24-preview 366 7/1/2022
2.0.23-preview 564 6/21/2022
2.0.22-preview 195 6/21/2022
2.0.21-preview 239 6/15/2022
2.0.20-preview 204 6/8/2022
2.0.19-preview 187 6/4/2022
2.0.18-preview 271 5/31/2022
2.0.17-preview 177 5/30/2022
2.0.15-preview 463 5/10/2022
2.0.14-preview 177 5/10/2022
2.0.13-preview 237 4/26/2022
2.0.12-preview 196 4/25/2022
2.0.11-preview 184 4/25/2022
2.0.10-preview 198 4/24/2022
2.0.9-preview 178 4/23/2022
2.0.8-preview 192 4/23/2022
2.0.7-preview 241 4/21/2022
2.0.6-preview 200 4/20/2022
2.0.5-preview 201 4/20/2022
2.0.4-preview 177 4/15/2022
2.0.3-preview 187 4/10/2022
2.0.2-preview 176 4/9/2022
2.0.1-preview 191 4/9/2022
1.1.7-preview 194 4/7/2022
1.1.6-preview 347 2/10/2022
1.1.5-preview 186 2/10/2022
1.1.4-preview 188 2/10/2022
1.1.3-preview 182 2/10/2022
1.1.2-preview 185 2/10/2022
1.0.5 7,576 2/10/2022
1.0.4-preview 198 2/10/2022
1.0.3-preview 200 2/10/2022
1.0.2-preview 183 2/10/2022
0.2.6-preview 198 2/10/2022
0.2.5-preview 207 2/5/2022
0.2.4-preview 351 10/23/2021
0.2.3-preview 305 10/23/2021
0.1.16 2,098 10/23/2021
0.1.14-preview 380 6/9/2021
0.1.13-preview 348 5/26/2021
0.1.12-preview 327 5/25/2021
0.1.11-preview 324 5/25/2021
0.1.10-preview 311 5/23/2021
0.1.9-preview 398 5/21/2021
0.1.8-preview 318 5/20/2021
0.1.7-preview 318 5/20/2021
0.1.6-preview 325 5/20/2021
0.1.5-preview 361 5/20/2021
0.1.4-preview 377 5/20/2021
0.1.3-preview 286 5/20/2021