Thinktecture.Runtime.Extensions
8.3.0
dotnet add package Thinktecture.Runtime.Extensions --version 8.3.0
NuGet\Install-Package Thinktecture.Runtime.Extensions -Version 8.3.0
<PackageReference Include="Thinktecture.Runtime.Extensions" Version="8.3.0" />
paket add Thinktecture.Runtime.Extensions --version 8.3.0
#r "nuget: Thinktecture.Runtime.Extensions, 8.3.0"
// Install Thinktecture.Runtime.Extensions as a Cake Addin #addin nuget:?package=Thinktecture.Runtime.Extensions&version=8.3.0 // Install Thinktecture.Runtime.Extensions as a Cake Tool #tool nuget:?package=Thinktecture.Runtime.Extensions&version=8.3.0
This library provides some interfaces, classes, Roslyn Source Generators, Roslyn Analyzers and Roslyn CodeFixes for implementation of Smart Enums, Value Objects and Discriminated Unions.
Documentation
See wiki for more documentation.
Requirements
- Version 8:
- C# 11 (or higher) for generated code
- SDK 8.0.400 (or higher) for building projects
- Version 7:
- C# 11 (or higher) for generated code
- SDK 7.0.401 (or higher) for building projects
Migrations
Ideas and real-world use cases
Smart Enums:
Value objects:
Smart Enums
Smart Enums provide a powerful alternative to traditional C# enums, offering type-safety, extensibility, and rich behavior. Unlike regular C# enums which are limited to numeric values and lack extensibility, Smart Enums can:
- Use any type as the underlying type (e.g., strings, integers) or none at all
- Include additional fields, properties and behavior
- Use polymorphism to define custom behavior for each value
- Prevent creation of invalid values
- Integrate seamlessly with JSON serializers, MessagePack, Entity Framework Core and ASP.NET Core
Install: Install-Package Thinktecture.Runtime.Extensions
Documentation: Smart Enums
Some of the Key Features are:
- Choice between always-valid and maybe-valid Smart Enum
- Reflection-free iteration over all items
- Fast lookup/conversion from underlying type to Smart Enum and vice versa
- Allows custom properties and methods
- Exhaustive pattern matching with
Switch
/Map
methods - Provides appropriate constructor, based on the specified properties/fields
- Proper implementation of
Equals
,GetHashCode
,ToString
and equality operators - Provides implementation of
IComparable
,IComparable<T>
,IFormattable
,IParsable<T>
and comparison operators<
,<=
,>
,>=
(if applicable to the underlying type) - Custom comparer and equality comparer
Roslyn Analyzers and CodeFixes help the developers to implement the Smart Enums correctly
Provides support for:
- JSON (System.Text.Json and Newtonsoft)
- Minimal Api Parameter Binding and ASP.NET Core Model Binding
- Entity Framework Core
- MessagePack
Definition of a Smart Enum with custom properties and methods.
[SmartEnum<string>]
public partial class ShippingMethod
{
public static readonly ShippingMethod Standard = new(
"STANDARD",
basePrice: 5.99m,
weightMultiplier: 0.5m,
estimatedDays: 5,
requiresSignature: false);
public static readonly ShippingMethod Express = new(
"EXPRESS",
basePrice: 15.99m,
weightMultiplier: 0.75m,
estimatedDays: 2,
requiresSignature: true);
public static readonly ShippingMethod NextDay = new(
"NEXT_DAY",
basePrice: 29.99m,
weightMultiplier: 1.0m,
estimatedDays: 1,
requiresSignature: true);
private readonly decimal _basePrice;
private readonly decimal _weightMultiplier;
private readonly int _estimatedDays;
public bool RequiresSignature { get; }
public decimal CalculatePrice(decimal orderWeight)
{
return _basePrice + (orderWeight * _weightMultiplier);
}
public DateTime GetEstimatedDeliveryDate()
{
return DateTime.Today.AddDays(_estimatedDays);
}
}
Behind the scenes a Roslyn Source Generator generates additional code. Some of the features that are now available are ...
Basic Operations
[SmartEnum<string>]
public partial class ProductType
{
// The source generator creates a private constructor
public static readonly ProductType Groceries = new("Groceries");
}
// Enumeration over all defined items
IReadOnlyList<ProductType> allTypes = ProductType.Items;
// Value retrieval
ProductType productType = ProductType.Get("Groceries"); // Get by key (throws if not found)
ProductType productType = (ProductType)"Groceries"; // Same as above but by using a cast
bool found = ProductType.TryGet("Groceries", out var productType); // Safe retrieval (returns false if not found)
// Validation with detailed error information
ValidationError? error = ProductType.Validate("Groceries", null, out ProductType? productType);
// IParsable<T> (useful for Minimal APIs)
bool parsed = ProductType.TryParse("Groceries", null, out ProductType? parsedType);
// IFormattable (e.g. for numeric keys)
string formatted = ProductGroup.Fruits.ToString("000", CultureInfo.InvariantCulture); // "001"
// IComparable
int comparison = ProductGroup.Fruits.CompareTo(ProductGroup.Vegetables);
bool isGreater = ProductGroup.Fruits > ProductGroup.Vegetables; // Comparison operators
Type Conversion and Equality
// Implicit conversion to key type
string key = ProductType.Groceries; // Returns "Groceries"
// Equality comparison
bool equal = ProductType.Groceries.Equals(ProductType.Groceries);
bool equal = ProductType.Groceries == ProductType.Groceries; // Operator overloading
bool notEqual = ProductType.Groceries != ProductType.Housewares;
// Methods inherited from Object
int hashCode = ProductType.Groceries.GetHashCode();
string key = ProductType.Groceries.ToString(); // Returns "Groceries"
// TypeConverter
var converter = TypeDescriptor.GetConverter(typeof(ProductType));
string? keyStr = (string?)converter.ConvertTo(ProductType.Groceries, typeof(string));
ProductType? converted = (ProductType?)converter.ConvertFrom("Groceries");
Pattern Matching with Switch/Map
All Switch
/Map
methods are exhaustive by default ensuring all cases are handled correctly.
ProductType productType = ProductType.Groceries;
// Execute different actions based on the enum value (void return)
productType.Switch(
groceries: () => Console.WriteLine("Processing groceries order"),
housewares: () => Console.WriteLine("Processing housewares order")
);
// Transform enum values into different types
string department = productType.Switch(
groceries: () => "Food and Beverages",
housewares: () => "Home and Kitchen"
);
// Direct mapping to values
// Direct mapping to values - clean and concise
decimal discount = productType.Map(
groceries: 0.05m, // 5% off groceries
housewares: 0.10m // 10% off housewares
);
For optimal performance Smart Enums provide overloads that prevent closures.
ILogger logger = ...;
// Prevent closures by passing the parameter as first method argument
productType.Switch(logger,
groceries: static l => l.LogInformation("Processing groceries order"),
housewares: static l => l.LogInformation("Processing housewares order")
);
// Use a tuple to pass multiple values
var context = (Logger: logger, OrderId: "123");
productType.Switch(context,
groceries: static ctx => ctx.Logger.LogInformation("Processing groceries order {OrderId}", ctx.OrderId),
housewares: static ctx => ctx.Logger.LogInformation("Processing housewares order {OrderId}", ctx.OrderId)
);
Value Objects
Install: Install-Package Thinktecture.Runtime.Extensions
Documentation: Value Objects
Features:
- Roslyn Analyzers and CodeFixes help the developers to implement the Value Objects correctly
- Choice between Simple Value Objects and Complex Value Objects
- Allows custom fields, properties and methods
- Provides appropriate factory methods for creation of new value objects based on the specified properties/fields
- Factory methods can be renamed
- Allows custom validation of constructor and factory method arguments
- Allows custom type to pass validation error(s)
- [Simple Value Objects only] Allows cast and type conversion from key-member type to Value Object and vice versa
- [Simple Value Objects only] Provides an implementation of
IFormattable
if the key member is anIFormattable
- Provides proper implementation of
Equals
,GetHashCode
,ToString
and equality comparison via==
and!=
- Provides implementation of
IComparable
,IComparable<T>
,IFormattable
,IParsable<T>
and comparison operators<
,<=
,>
,>=
- Allows custom equality comparison and custom comparer
- Configurable handling of null and empty strings
- JSON support (
System.Text.Json
andNewtonsoft.Json
) - Support for Minimal Api Parameter Binding and ASP.NET Core Model Binding
- Entity Framework Core support (
ValueConverter
) - MessagePack support (
IMessagePackFormatter
) - Logging for debugging or getting insights
Simple Value Object
A simple value object has 1 field/property only, i.e., it is kind of wrapper for another (primitive) type. The main use case is to prevent creation of values/instances which are considered invalid according to some rules. In DDD (domain-driven design), working with primitive types, like string, directly is called primitive obsession and should be avoided.
Most simple value objects with a key member of type string
and another one (which is a struct) with an int
.
[ValueObject<string>]
public sealed partial class ProductName
{
}
[ValueObject<int>]
public readonly partial struct Amount
{
}
After the implementation of a value object, a Roslyn source generator kicks in and implements the rest. Following API is available from now on.
// Factory method for creation of new instances.
// Throws ValidationException if the validation fails (if we had any)
ProductName apple = ProductName.Create("Apple");
// Alternatively, using an explicit cast, which behaves the same way as calling "ProductName.Create"
ProductName apple = (ProductName)"Apple";
-----------
// The same as above but returns a bool instead of throwing an exception (dictionary-style)
bool created = ProductName.TryCreate("Chocolate", out ProductName? chocolate);
-----------
// Similar to TryCreate but returns a ValidationError instead of a boolean.
ValidationError? validationError = ProductName.Validate("Chocolate", null, out var chocolate);
if (validationError is null)
{
logger.Information("Product name {Name} created", chocolate);
}
else
{
logger.Warning("Failed to create product name. Validation result: {validationError}", validationError.ToString());
}
-----------
// Implicit conversion to the type of the key member
string valueOfTheProductName = apple; // "Apple"
-----------
// Equality comparison compares the key member using default comparer by default.
// Key members of type `string` are compared with 'StringComparer.OrdinalIgnoreCase' by default.
bool equal = apple.Equals(apple);
-----------
// Equality comparison operators: '==' and '!='
bool equal = apple == apple;
bool notEqual = apple != apple;
-----------
// Hash code: combined hash code of type and key member.
// Strings are using 'StringComparer.OrdinalIgnoreCase' by default.
int hashCode = apple.GetHashCode();
-----------
// 'ToString' implementation return the string representation of the key member
string value = apple.ToString(); // "Apple"
------------
// Implements IParsable<T> which is especially helpful with minimal apis.
bool success = ProductName.TryParse("New product name", null, out var productName);
ProductName productName = ProductName.Parse("New product name", null);
------------
// Implements "IFormattable" if the key member is an "IFormattable".
Amount amount = Amount.Create(42);
string formattedValue = amount.ToString("000", CultureInfo.InvariantCulture); // "042"
------------
// Implements "IComparable<ProductName>" if the key member is an "IComparable",
// or if custom comparer is provided.
Amount amount = Amount.Create(1);
Amount otherAmount = Amount.Create(2);
int comparison = amount.CompareTo(otherAmount); // -1
------------
// Implements comparison operators (<,<=,>,>=) if the key member has comparison operators itself.
bool isBigger = amount > otherAmount;
// Implements comparison operators to compare the value object with an instance of key-member-type,
// if "ComparisonOperators" is set "OperatorsGeneration.DefaultWithKeyTypeOverloads"
bool isBigger = amount > 2;
------------
// Implements addition / subtraction / multiplication / division if the key member supports corresponding operators
Amount sum = amount + otherAmount;
// Implements operators that accept an instance of key-member-type,
// if the "OperatorsGeneration" is set "DefaultWithKeyTypeOverloads"
Amount sum = amount + 2;
------------
// Provides a static default value "Empty" (similar to "Guid.Empty"), if the value object is a struct
Amount defaultValue = Amount.Empty; // same as "Amount defaultValue = default;"
Complex Value Objects
A complex value object is an immutable class
or a readonly struct
with a ComplexValueObjectAttribute
. Complex value object usually has multiple readonly fields/properties.
A simple example would be a Boundary
with 2 properties. One property is the lower boundary and the other is the upper boundary. Yet again, we skip the validation at the moment.
[ComplexValueObject]
public sealed partial class Boundary
{
public decimal Lower { get; }
public decimal Upper { get; }
}
The rest is implemented by a Roslyn source generator, providing the following API:
// Factory method for creation of new instances.
// Throws ValidationException if the validation fails (if we had any)
Boundary boundary = Boundary.Create(lower: 1, upper: 2);
-----------
// the same as above but returns a bool instead of throwing an exception (dictionary-style)
bool created = Boundary.TryCreate(lower: 1, upper: 2, out Boundary? boundary);
-----------
// similar to TryCreate but returns a ValidationError instead of a boolean.
ValidationError? validationError = Boundary.Validate(lower: 1, upper: 2, out Boundary? boundary);
if (validationError is null)
{
logger.Information("Boundary {Boundary} created", boundary);
}
else
{
logger.Warning("Failed to create boundary. Validation result: {validationError}", validationError.ToString());
}
-----------
// Equality comparison compares the members using default or custom comparers.
// Strings are compared with 'StringComparer.OrdinalIgnoreCase' by default.
bool equal = boundary.Equals(boundary);
-----------
// Equality comparison with '==' and '!='
bool equal = boundary == boundary;
bool notEqual = boundary != boundary;
-----------
// Hash code of the members according default or custom comparers
int hashCode = boundary.GetHashCode();
-----------
// 'ToString' implementation
string value = boundary.ToString(); // "{ Lower = 1, Upper = 2 }"
Discriminated Unions
Install: Install-Package Thinktecture.Runtime.Extensions
Documentation: Discriminated Unions
There are 2 types of unions: ad hoc union
and "regular" unions
Ad hoc unions
Features:
- Roslyn Analyzers and CodeFixes help the developers to implement the unions correctly
- Provides proper implementation of
Equals
,GetHashCode
,ToString
and equality comparison via==
and!=
- Switch-Case/Map
- Renaming of properties
- Definition of nullable reference types
Definition of a basic union with 2 types using a class
, a struct
or ref struct
:
// class
[Union<string, int>]
public partial class TextOrNumber;
// struct
[Union<string, int>]
public partial struct TextOrNumber;
// ref struct
[Union<string, int>]
public ref partial struct TextOrNumber;
// Up to 5 types
[Union<string, int, bool, Guid, char>]
public partial class MyUnion;
Behind the scenes a Roslyn Source Generator generates additional code. Some of the features that are now available are ...
// Implicit conversion from one of the defined generics.
TextOrNumber textOrNumberFromString = "text";
TextOrNumber textOrNumberFromInt = 42;
// Check the type of the value.
// By default, the properties are named using the name of the type (`String`, `Int32`)
bool isText = textOrNumberFromString.IsString;
bool isNumber = textOrNumberFromString.IsInt32;
// Getting the typed value.
// Throws "InvalidOperationException" if the current value doesn't match the calling property.
// By default, the properties are named using the name of the type (`String`, `Int32`)
string text = textOrNumberFromString.AsString;
int number = textOrNumberFromInt.AsInt32;
// Alternative approach is to use explicit cast.
// Behavior is identical to methods "As..."
string text = (string)textOrNumberFromString;
int number = (int)textOrNumberFromInt;
// Getting the value as object, i.e. untyped.
object value = textOrNumberFromString.Value;
// Implementation of Equals, GetHashCode and ToString
// PLEASE NOTE: Strings are compared using "StringComparison.OrdinalIgnoreCase" by default! (configurable)
bool equals = textOrNumberFromInt.Equals(textOrNumberFromString);
int hashCode = textOrNumberFromInt.GetHashCode();
string toString = textOrNumberFromInt.ToString();
// Equality comparison operators
bool equal = textOrNumberFromInt == textOrNumberFromString;
bool notEqual = textOrNumberFromInt != textOrNumberFromString;
There are multiple overloads of switch-cases: with Action
, Func<T>
and concrete values.
To prevent closures, you can pass a value to method Switch
, which is going to be passed to provided callback (Action
/Func<T>
).
By default, the names of the method arguments are named after the type specified by UnionAttribute<T1, T2>
.
Reserved C# keywords (like string
) must string with @
(like @string
, @default
, etc.).
// With "Action"
textOrNumberFromString.Switch(@string: s => logger.Information("[Switch] String Action: {Text}", s),
int32: i => logger.Information("[Switch] Int Action: {Number}", i));
// With "Action". Logger is passed as additional parameter to prevent closures.
textOrNumberFromString.Switch(logger,
@string: static (l, s) => l.Information("[Switch] String Action with logger: {Text}", s),
int32: static (l, i) => l.Information("[Switch] Int Action with logger: {Number}", i));
// With "Func<T>"
var switchResponse = textOrNumberFromInt.Switch(@string: static s => $"[Switch] String Func: {s}",
int32: static i => $"[Switch] Int Func: {i}");
// With "Func<T>" and additional argument to prevent closures.
var switchResponseWithContext = textOrNumberFromInt.Switch(123.45,
@string: static (value, s) => $"[Switch] String Func with value: {ctx} | {s}",
int32: static (value, i) => $"[Switch] Int Func with value: {ctx} | {i}");
// Use `Map` instead of `Switch` to return concrete values directly.
var mapResponse = textOrNumberFromString.Map(@string: "[Map] Mapped string",
int32: "[Map] Mapped int");
Use T1Name
/T2Name
of the UnionAttribute
to get more meaningful names.
[Union<string, int>(T1Name = "Text",
T2Name = "Number")]
public partial class TextOrNumber;
The properties and method arguments are renamed accordingly:
bool isText = textOrNumberFromString.IsText;
bool isNumber = textOrNumberFromString.IsNumber;
string text = textOrNumberFromString.AsText;
int number = textOrNumberFromInt.AsNumber;
textOrNumberFromString.Switch(text: s => logger.Information("[Switch] String Action: {Text}", s),
number: i => logger.Information("[Switch] Int Action: {Number}", i));
Regular unions
Features:
- Roslyn Analyzers and CodeFixes help the developers to implement the unions correctly
- Can be a
class
orrecord
- Switch-Case/Map
- Supports generics
- Derived types can be simple classes or something complex like a value object.
Simple union using a class and a value object:
[Union]
public partial class Animal
{
[ValueObject<string>]
public partial class Dog : Animal;
public sealed class Cat : Animal;
}
Similar example as above but with records
:
[Union]
public partial record AnimalRecord
{
public sealed record Dog(string Name) : AnimalRecord;
public sealed record Cat(string Name) : AnimalRecord;
}
A union type (i.e. the base class) with a property:
[Union]
public partial class Animal
{
public string Name { get; }
private Animal(string name)
{
Name = name;
}
public sealed class Dog(string Name) : Animal(Name);
public sealed class Cat(string Name) : Animal(Name);
}
A record
with a generic:
[Union]
public partial record Result<T>
{
public record Success(T Value) : Result<T>;
public record Failure(string Error) : Result<T>;
public static implicit operator Result<T>(T value) => new Success(value);
public static implicit operator Result<T>(string error) => new Failure(error);
}
One of the main purposes for a regular union is their exhaustiveness, i.e. all member types are accounted for in a switch/map:
Animal animal = new Animal.Dog("Milo");
animal.Switch(dog: d => logger.Information("Dog: {Dog}", d),
cat: c => logger.Information("Cat: {Cat}", c));
var result = animal.Map(dog: "Dog",
cat: "Cat");
Use flags SwitchMethods
and MapMethods
for generation of SwitchPartially
/MapPartially
:
[Union(SwitchMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads,
MapMethods = SwitchMapMethodsGeneration.DefaultWithPartialOverloads)]
public partial record AnimalRecord
{
public sealed record Dog(string Name) : AnimalRecord;
public sealed record Cat(string Name) : AnimalRecord;
}
---------------------------
Animal animal = new Animal.Dog("Milo");
animal.SwitchPartially(@default: a => logger.Information("Default: {Animal}", a),
cat: c => logger.Information("Cat: {Cat}", c.Name));
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net7.0 is compatible. 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. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. |
-
net7.0
-
net9.0
NuGet packages (10)
Showing the top 5 NuGet packages that depend on Thinktecture.Runtime.Extensions:
Package | Downloads |
---|---|
Thinktecture.Runtime.Extensions.Json
Adds better JSON support to components from Thinktecture.Runtime.Extensions when using System.Text.Json. |
|
Thinktecture.Runtime.Extensions.AspNetCore
Adds better validation support to components from Thinktecture.Runtime.Extensions when using them along with ASP.NET Core. |
|
Thinktecture.Runtime.Extensions.EntityFrameworkCore
Extends Entity Framework Core to support some components from Thinktecture.Runtime.Extensions. |
|
Thinktecture.Runtime.Extensions.EntityFrameworkCore8
Extends Entity Framework Core to support some components from Thinktecture.Runtime.Extensions. |
|
Thinktecture.Runtime.Extensions.Newtonsoft.Json
Adds better JSON support to components from Thinktecture.Runtime.Extensions when using Newtonsoft.Json. |
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
8.3.0 | 0 | 2/16/2025 |
8.2.0 | 146 | 1/23/2025 |
8.1.0 | 167 | 1/17/2025 |
8.0.2 | 457 | 12/18/2024 |
8.0.1 | 264 | 12/11/2024 |
8.0.0 | 210 | 12/9/2024 |
8.0.0-beta13 | 147 | 11/21/2024 |
8.0.0-beta12 | 142 | 11/19/2024 |
8.0.0-beta11 | 139 | 11/15/2024 |
8.0.0-beta10 | 180 | 10/25/2024 |
8.0.0-beta09 | 201 | 10/23/2024 |
8.0.0-beta08 | 141 | 10/23/2024 |
8.0.0-beta07 | 151 | 9/27/2024 |
8.0.0-beta06 | 219 | 9/19/2024 |
8.0.0-beta05 | 193 | 9/9/2024 |
8.0.0-beta04 | 175 | 9/9/2024 |
8.0.0-beta03 | 170 | 9/8/2024 |
8.0.0-beta02 | 164 | 9/3/2024 |
8.0.0-beta01 | 177 | 8/13/2024 |
7.6.1 | 372 | 11/21/2024 |
7.6.0 | 8,097 | 11/13/2024 |
7.5.3 | 1,294 | 10/23/2024 |
7.5.2 | 5,711 | 9/27/2024 |
7.5.1 | 473 | 9/5/2024 |
7.5.0 | 2,017 | 7/9/2024 |
7.4.0 | 932 | 6/13/2024 |
7.3.0 | 574 | 4/18/2024 |
7.2.1 | 227 | 4/14/2024 |
7.2.0 | 23,753 | 1/31/2024 |
7.1.0 | 3,968 | 12/11/2023 |
7.0.0 | 283 | 12/10/2023 |
7.0.0-beta10 | 222 | 11/30/2023 |
7.0.0-beta09 | 211 | 11/26/2023 |
7.0.0-beta08 | 228 | 11/19/2023 |
7.0.0-beta07 | 212 | 11/17/2023 |
7.0.0-beta06 | 208 | 11/14/2023 |
7.0.0-beta05 | 177 | 11/14/2023 |
7.0.0-beta04 | 168 | 11/7/2023 |
7.0.0-beta03 | 193 | 10/19/2023 |
7.0.0-beta02 | 186 | 10/10/2023 |
7.0.0-beta01 | 179 | 10/8/2023 |
6.6.0 | 260 | 11/30/2023 |
6.5.2 | 312 | 11/17/2023 |
6.5.1 | 228 | 11/11/2023 |
6.5.0 | 225 | 11/11/2023 |
6.5.0-beta03 | 182 | 11/6/2023 |
6.5.0-beta02 | 175 | 11/5/2023 |
6.5.0-beta01 | 175 | 11/5/2023 |
6.4.1 | 222 | 11/9/2023 |
6.4.0 | 2,160 | 9/3/2023 |
6.3.0 | 368 | 8/31/2023 |
6.2.0 | 2,686 | 4/2/2023 |
6.1.0 | 410 | 3/22/2023 |
6.1.0-beta02 | 210 | 3/19/2023 |
6.1.0-beta01 | 212 | 3/19/2023 |
6.0.1 | 395 | 3/12/2023 |
6.0.0 | 381 | 3/9/2023 |
6.0.0-beta03 | 213 | 3/6/2023 |
6.0.0-beta02 | 208 | 3/5/2023 |
6.0.0-beta01 | 221 | 3/2/2023 |
5.2.0 | 1,600 | 2/6/2023 |
5.1.0 | 33,109 | 11/25/2022 |
5.0.1 | 3,520 | 10/6/2022 |
5.0.0 | 1,599 | 9/28/2022 |
5.0.0-beta03 | 305 | 9/7/2022 |
5.0.0-beta02 | 253 | 9/4/2022 |
5.0.0-beta01 | 682 | 9/4/2022 |
4.4.0-beta02 | 608 | 8/31/2022 |
4.4.0-beta01 | 587 | 8/30/2022 |
4.3.3 | 35,867 | 8/17/2022 |
4.3.3-beta01 | 645 | 8/17/2022 |
4.3.2 | 5,596 | 6/15/2022 |
4.3.2-beta02 | 683 | 6/15/2022 |
4.3.2-beta01 | 657 | 5/18/2022 |
4.3.1 | 12,238 | 5/17/2022 |
4.3.0 | 2,018 | 5/16/2022 |
4.2.0 | 3,133 | 4/24/2022 |
4.2.0-beta01 | 659 | 4/23/2022 |
4.1.3 | 2,016 | 4/13/2022 |
4.1.2 | 3,904 | 2/10/2022 |
4.1.1 | 2,008 | 2/6/2022 |
4.1.0 | 2,099 | 2/5/2022 |
4.0.1 | 38,005 | 1/16/2022 |
4.0.0 | 1,938 | 1/14/2022 |
4.0.0-beta03 | 673 | 12/17/2021 |
4.0.0-beta02 | 673 | 12/14/2021 |
4.0.0-beta01 | 672 | 12/13/2021 |
3.1.0 | 3,767 | 9/25/2021 |
3.0.0 | 1,885 | 7/10/2021 |
3.0.0-beta10 | 946 | 7/10/2021 |
3.0.0-beta09 | 828 | 4/1/2021 |
3.0.0-beta08 | 963 | 3/15/2021 |
3.0.0-beta07 | 830 | 3/14/2021 |
3.0.0-beta06 | 901 | 3/11/2021 |
3.0.0-beta05 | 1,392 | 2/12/2021 |
3.0.0-beta04 | 817 | 2/7/2021 |
3.0.0-beta03 | 822 | 1/30/2021 |
3.0.0-beta02 | 930 | 1/11/2021 |
3.0.0-beta01 | 886 | 1/10/2021 |
2.1.0-beta02 | 1,018 | 11/6/2020 |
2.1.0-beta01 | 337 | 11/6/2020 |
2.0.1 | 53,064 | 10/8/2019 |
2.0.0 | 1,406 | 10/8/2019 |
2.0.0-beta02 | 902 | 10/8/2019 |
2.0.0-beta01 | 1,012 | 9/30/2019 |
1.2.0 | 3,607 | 9/29/2019 |
1.2.0-beta04 | 1,389 | 8/29/2019 |
1.2.0-beta03 | 1,082 | 8/26/2019 |
1.2.0-beta02 | 1,051 | 8/12/2019 |
1.2.0-beta01 | 996 | 8/4/2019 |
1.1.0 | 5,160 | 5/31/2018 |
1.0.0 | 1,132 | 2/26/2018 |
0.1.0-beta5 | 837 | 2/23/2018 |
0.1.0-beta4 | 812 | 2/22/2018 |
0.1.0-beta3 | 833 | 2/19/2018 |
0.1.0-beta2 | 852 | 2/16/2018 |
0.1.0-beta1 | 895 | 2/11/2018 |