wan24-Core 1.18.2

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

// Install wan24-Core as a Cake Tool
#tool nuget:?package=wan24-Core&version=1.18.2                

wan24-Core

This core library contains some .NET extensions:

  • Bootstrapping
  • Disposable base class for disposable types, which supports asynchronous disposing
    • Dispose attribute for fields/properties which should be disposed automatic when disposing
  • CancellationOnDispose cancels a cancellation token when an object is being disposed (or another given cancellation token ws canceled)
  • Cancellations combines multiple cancellation tokens into one
  • Type helper (type loading)
  • Secure byte and char array, which clears its contents when disposing
  • Pool rented array as disposable object (which optionally clears its contents when disposing; for byte/char arrays just like the Secure*Array)
  • Byte array extensions
    • Endian conversion
    • Bit-converter (endian-safe)
    • UTF-8/16/32 (little endian) string decoding
    • Clearing
  • Char array extensions
    • Clearing
  • Array helper extensions
    • Offset/length validation
  • Array pool extensions
    • Renting a cleared array
  • Enumerable extensions
    • Combine enumerables
    • Chunk enumerables
  • Reflection extensions
    • Automatic parameter extension when invoking a method (with DI support)
    • Synchronous/asynchronous method invokation
    • Automatic constructor invokation using a given parameter set (with DI support)
    • Nullability detection
  • Delegate extensions
    • Delegate list invokation (with or without return values, with DI support)
    • Asynchronous delegate list invokation (with or without return values, with DI support)
  • Task extensions
    • Result getting of a generic task
    • Asynchronous task list awaiting
    • Shortcuts for await configurations
    • Shortcuts for starting a function as long running task
    • Shortcuts for starting a function as task with fair execution by the scheduler
    • Add a cancellation token to a task (which can cancel the task awaiter)
  • DI helper
    • Service provider adoption
    • DI object factory delegates
    • Asynchronous DI object factory delegates
  • Enumeration extensions
    • Get enumeration value display string from DisplayTextAttribute or using ToString (fallback)
    • Determine if all, any or which flags are contained in an enumeration value
    • Remove flags of a mixed enumeration value
    • Get only flags of a mixed enumeration value
    • Value validation
  • Number extensions
    • Determine if a type is a number
    • Determine if a number type is unsigned
    • Bit-converter (endian-safe)
    • Determine if a number (or any IComparable) is within a range
  • Numeric bitwise extensions
  • Collection extensions
    • Add a range of items
  • JSON helper
    • Exchangeable JSON encoder/decoder delegates (using System.Text.Json per default)
  • JSON extensions
    • Encode an object
    • Decode from a type
    • Decode a string
  • Object extensions
    • Type conversion
    • Determine if a value is within a list of values
  • String extensions
    • Get UTF-8/16/32 bytes (little endian)
  • Generic helper
    • Determine if two generic values are equal
    • Determine if a value is null
    • Determine if a value is default
    • Determine if a value is null or default
  • DateTime extensions
    • Determine if a time is within a range
    • Determine if a time matches a reference time plus/minus an offset
    • Apply an offset to a time base on a reference time
  • TimeSpanHelper
    • Update a timeout
  • Queue worker (for actions and/or items)
  • Parallel queue worker (for actions and/or items)
  • ParallelAsync implementation
    • ForEachAsync with an asynchronous or synchronous input source
    • FilterAsync with an asynchronous or synchronous input source and item filter
    • Filter for synchronous parallel filtering
  • Base class for a hosted worker, which implements the IHostedService interface (timed or permanent running)
  • EventThrottle for throttling event handler calls
  • ProcessThrottle for throttling a processing channel
  • OrderedDictionary<tKey, tValue> is used for working with indexed key/value pairs
  • Timeout will count down and raise an event, if not reset before reaching the timeout
  • ILogger support
  • IChangeToken support using ChangeCallback
  • Hierarchic configuration using OverrideableConfig
  • Cancellation token awaiter
  • ObjectPool for pooling objects (DisposableObjectPool for disposable types), and BlockingObjectPool for a strict pool capacity limit
  • ResetEvent for (a)synchronous event waiting
  • LazyValue<T>, DisposableLazyValue<T>, AsyncDisposableLazyValue<T> and TimeoutValue<T> for lazy and timeout value serving
  • ObjectLockManager<T> for asynchronous and awaitable object locking
  • Bitmap for working with bits
  • DisposableWrapper<T> for wrapping any (not disposable?) object with the IDisposable and IAsyncDisposable interface using custom dispose actions during runtime
  • DisposableAdapter for adopting the IDisposableObject interface from a type which can't extend the DisposableBase type
  • Generic object extenions for validating method arguments
  • CLI arguments interpreter

How to get it

This library is available as NuGet package "wan24-Core".

Bootstrapping

The Bootstrapper.Async method calls all static methods having the BootstrapperAttribute. In order to be able to find the methods, it's required to add the BootstrapperAttribute to the assembly.

You may also ad the BootstrapperAttribute to a type and/or the bootstrapper method, in case the assembly contains multiple of them. In the assembly attribute you need to set ScanClasses and/or ScanMethods to true in order to perform a deep scanning during bootstrapping for performance reasons.

The bootstrapper methods may consume parameters which are available from the DI helper. The method may be synchronous or asynchronous. The method can't be defined in a generic class, and it can't be generic itself.

[assembly:Bootstrapper(typeof(YourBootstrapper),nameof(YourBootstrapper.BootstrapperMethod))]

public static class YourBootstrapper
{
    public static async Task BootstrapperMethod()
    {
        // Perform your bootstrapping here
    }
}

// Call the bootstrapper somewhere in your apps initialization code
await Bootstrap.Async();

The BootstrapperAttribute can be initialized with a numeric priority. The bootstrapper will order the found bootstrapping methods by priority, where the one with the highest number will be executed first (assembly and type priorities count, too). At last there's a assembly location, type and method name sorting. Bootstrapper methods will be executed sequential.

If you give a type and a method name to the assembly BootstrapperAttribute, you won't need to add the attribute to the type and the method.

During bootstrapping, the cancellation token which was given to the Bootstrap.Async method, can be injected to a bootstrappers method parameters.

After that bootstrapping was done, the Bootstrap.AsyncBootstrapper will be called. At last the Bootstrap.OnBootstrap event will be raised.

During bootstrapping the Bootstrap.IsBooting property is true. After bootstrapping the Bootstrap.DidBoot property is true.

The bootstrapper will load all referenced assemblies. If you load an assembly later, it'll be bootstrapped automatic and added to the TypeHelper singleton instance.

Type helper

If you use the TypeHelper.AddTypes method, the unknown assemblies of the added types will be added as searchable assemblies automatic.

You may attach to the TypeHelper.OnLoadType event for handling requests more dynamic.

The TypeHelper.GetType method will try Type.GetType first and fall back to the helper, if no type was found.

DI helper

In order to make DI (dependency injection) working, you need to

  • set a DiHelper.ServiceProvider and/or
  • add DiHelper.(Async)ObjectFactories

The DiHelper.GetDiObjectAsync method will try to resolve the request synchronous, first. But the DiHelper.GetDiObject won't try asynchronous object factories.

Mixed enumeration value

A mixed enumeration contains X bits enumeration values, and Y bits flags:

[Flags]
public enum MixedEnum : int
{
    None = 0,
    Value1 = 1,
    Value2 = 2,
    Value3 = 3,
    ...
    Flag1 = 1 << 8,
    Flag2 = 1 << 9,
    FLAGS = Flag1 | Flag2 // Required to identify flags
}

The FLAGS value helps these extension methods to handle flag values:

MixedEnum value = MixedEnum.Value1 | MixedEnum.Flag1,
    valueOnly = value.RemoveFlags(),// == MixedEnum.Value1
    flagsOnly = value.OnlyFlags();// == MixedEnum.Flag1

Unsafe code

The library uses unsafe code. If you don't want/need that, you can compile the library with the NO_UNSAFE compiler constant to disable any unsafe operation. Remember to unset the unsafe compiler option, too!

Disposable base class

The DisposableBase implements the IDisposable and IAsyncDisposable interfaces. It provides some helpers and events, and also the DisposeAttribute, which can be applied to fields and properties which you wish to dispose automatic when disposing.

When your type derives from the DisposableBase, you'll need to implement the abstract Dispose method:

protected override Dispose(bool disposing)
{
    // Your dispose logic here
}

There are measures to avoid that this method is being called twice.

To implement custom asynchronous disposing:

protected override async Task DisposeCore()
{
    // Your dispose logic here
}

In order to make the DisposeAttribute working, you have to call the protected method DisposeAttributes or DisposeAttributesAsync.

The IsDisposing property value will be true as soon as the disposing process started, and it will never become false again. The IsDisposed property value will be true as soon as the disposing process did finish.

Queue worker

using QueueWorker worker = new();
await worker.EnqueueAsync((ct) =>
{
    // Do any background action here
});

The QueueWorker class can be extended as you need it.

The ParallelQueueWorker requires a number of threads in the constructor, which defines the degree of parallelism, in which enqueued tasks will be processed.

Queue item worker

using QueueItemWorker<ItemType> worker = new();
await worker.EnqueueAsync(new ItemType());

The QueueItemWorker<T> class can be extended as you need it.

The ParallelItemQueueWorker<T> requires a number of threads in the constructor, which defines the degree of parallelism, in which enqueued items will be processed.

ParallelAsync

Using the .NET parallel implementation it's not possible to invoke asynchronous item handlers. For this you can use the ParallelAsync.ForEachAsync method, which uses a parallel item queue worker in the background for asynchronous processing.

Hosted worker

public class YourHostedWorker : HostedWorkerBase
{
    public YourHostedWorker() : base() { }

    protected override async Task WorkerAsync()
    {
        // Perform the service actions here
    }
}

The hosted worker implements the IHostedService interface and can be extended as you need it.

Timed hosted worker

public class YourHostedWorker : TimedHostedWorkerBase
{
    public YourHostedWorker() : base(interval: 500) { }

    protected override async Task WorkerAsync()
    {
        // Perform the service actions here
    }
}

This example uses a 500ms timer. Based on the defined timer type, the interval will be processed in different ways:

  • Default: Next worker run is now plus the interval (used by default)
  • Exact: Next worker run is now plus the interval minus the processing duration (used, if the start time of the processing is important)
  • ExactCatchingUp: As Exact, but catching up missing processing runs without delay, if a worker run duration exceeds the interval (used, if the number of worker runs is important)

Using the SetTimerAsync method you can change the timer settings at any time. If you give the nextRun parameter, you may set a fixed next run time (which won't effect the given interval, but just force the service to run at a specific time for the next time).

NOTE: The nextRun parameter will also force the service to (re)start!

By setting the property RunOnce to true, the service will stop after running the worker once. In combination with the SetTimerAsync parameter nextRun you can execute the worker at a specific time once.

The hosted worker implements the IHostedService interface and can be extended as you need it.

EventThrottle

public class YourType : DisposableBase
{
    protected readonly YourEventThrottle EventThrottle;

    public YourType() : base() => EventThrottle = new(this);

    // This method will raise the OnEvent
    public void AnyMethod()
    {
        RaiseOnEventThrottled();
    }

    protected override Dispose(bool disposing) => EventThrottle.Dispose();

    // Delegate for OnEvent
    public delegate void YourTypeEvent_Delegate();
    // Event to throttle
    public event YourTypeEvent_Delegate? OnEvent;
    // Raise the OnEvent using the event throttle
    protected void RaiseOnEventThrottled() => EventThrottle.Raise();
    // Finally let the event handlers process the event
    protected void RaiseOnEvent() => OnEvent?.Invoke();

    // Event throttle implementation
    public class YourEventThrottle : EventThrottle
    {
        // Throttle the event handling down to max. one handling per 300ms
        public YourEventThrottle(YourType instance) : base(timeout: 300) => Instance = instance;

        public YourType Instance { get; }

        protected override HandleEvent(DateTime raised, int raisedCount)
        {
            Instance.RaiseOnEvent();
        }
    }
}

If AnyMethod is being called, the event will be forwarded to the event throttle, which decides to throttle or raise the event. If AnyMethod was called three times within 300ms, the first call will be executed in realtime, while the 2nd and the 3rd call will be sqashed and executed once 300ms after the 1st call was processed.

This example assumes you're working with a real event - but you may throttle any event (which may not be a real event) using throttling logic.

ProcessThrottle

public class YourProcessThrottle : ProcessThrottle
{
    // Throttle to processing one object per second
    public YourProcessThrottle() : base(limit: 1, timeout: 1000) { }

    // Processing API using a timeout
    public async Task<int> ProcessAsync(Memory<bool> items, TimeSpan timeout)
        => await ProcessAsync(items.Length, (count) => 
        {
            await Task.Yield();
            Span<bool> toProcess = items.Span[..count];
            items = items[count..];
            // Process toProcess
        }, timeout);

    // Processing API using a cancellation token
    public async Task<int> ProcessAsync(Memory<bool> items, CancellationToken token = default)
        => await ProcessAsync(items.Length, (count) => 
        {
            await Task.Yield();
            Span<bool> toProcess = items.Span[..count];
            items = items[count..];
            // Process toProcess
        }, token);
}

The example will throttle the processing to a maximum of one object per second. Multiple threads may call ProcessAsync concurrent - processing will be organized thread-safe.

The return value of ProcessAsync is the number of objects processed until timeout or canceled.

The processing delegate shouldn't care about the timeout or if canceled and just process the given number of objects.

NOTE: A usage gap will slide the throttling timer. Example:

The timeout was set to 3 objects per 100ms. Now processing goes like this:

  • First processed object on 0ms will activate the throttling timeout
  • Next processed object on 10ms will increase the object throttling counter
  • Next processed object on 110ms will reset the throttling timeout and counter (the usage gap of 100ms does exceed the timeout)
  • Next 2 processed objects on 120ms will activate the throttle
  • Next object will have to wait until the throttle was released
  • The throttle will be released on 210ms, which allows the last object to be processed now

In short words: The throttle timer will not reset in an fixed interval, but the interval starts when processing items.

Change token

Implement by extending ChangeToken:

public class YourObservableType : ChangeToken
{
    public YourObservableType() : base()
    {
        ChangeIdentifier = () => HasChanged;
    }

    public bool HasChanged => ...;// Return if the object was changed

    public void ChangeAction()
    {
        // Perform changes
        InvokeCallbacks();
    }
}

Or by using a ChangeToken instance:

public class YourObservableType : IChangeToken
{
    public readonly ChangeToken ChangeToken;

    public YourObservableType() => ChangeToken = new(() => HasChanged);

    public bool HasChanged => ...;// Return if the object was changed

    public void ChangeAction()
    {
        // Perform changes
        ChangeToken.InvokeCallbacks();
    }

    // Implement the IChangeToken interface using our ChangeToken instance

    bool IChangeToken.HasChanged => ChangeToken.HasChanged;

    bool IChangeToken.ActiveChangeCallbacks => ChangeToken.ActiveChangeCallbacks;

    IDisposable IChangeToken.RegisterChangeCallback(Action<object?> callback, object? state)
        => ChangeToken.RegisterChangeCallback(callback, state);
}

Hierarchic configuration

Assume this configuration hierarchy:

Level Description
1 Default values
2 User values (can override default values)
3 Administrator values (can override default/user values)

In code:

public sealed class Config : OverrideableConfig<Config>
{
    public Config() : base()
    {
        SubConfig = new(this, new(this));// User values
        InitProperties();
    }

    private Config(Config parent, Config? sub = null) : base(parent)
    {
        if(sub != null)
        {
            SubConfig = sub;
            sub.ParentConfig = this;
            sub.SubConfig = new(sub);// Administrator values
        }
        InitProperties();
    }

    // A configuration value
    public ConfigOption<string, Config> AnyValue { get; private set; } = null!;

    private void InitProperties()
    {
        AnyValue = ParentConfig == null 
            // The master option has a default value
            ? new(this, nameof(AnyValue), canBeOverridden: true, "default")
            // No default value for a sub-option
            : new(this, nameof(AnyValue));
    }
}

Config config = new(),
    user = config.SubConfig,
    admin = user.SubConfig;

CAUTION: There's no endless-recursion protection for the ParentConfig or the SubConfig properties!

Now users are able to override default values, and administrators are able to override default and/or user values:

// Still the default value
Assert.AreEqual("default", config.AnyValue.FinalValue);

// User overrides the default value
user.AnyValue.Value = "user";
Assert.AreEqual("default", config.AnyValue.Value);
Assert.AreEqual("user", config.AnyValue.FinalValue);

// Administrator overrides the user value
admin.AnyValue.Value = "admin";
Assert.AreEqual("admin", config.AnyValue.FinalValue);

// User can't override the administrator value (but still store his own value 
// in case the administrator would unset his value)
user.AnyValue.Value = "test";
Assert.AreEqual("admin", config.AnyValue.FinalValue);
Assert.AreEqual("test", user.AnyValue.Value);

NOTE: Setting an option value is thread-safe.

It's also possible to flip the hierarchy:

Level Description
1 Default values
2 Administrator values (can define user visible and optional not overrideable values)
3 User values (can override overrideable values)

Using this hierarchy an administrator could also allow or deny overriding values at any time, for example.

The hierarchy depth isn't limited.

Object locking

The ObjectLockManager<T> helps locking any object during an asynchronous operation:

ObjectLock ol = await ObjectLockManager<AnyType>.Shared.LockAsync(anyObjectKey);
// A 2nd call to ObjectLockManager<AnyType>.Shared.LockAsync would block until the lock was released
await ol.RunTaskAsync(Task.Run(async () => 
{
    // Perform the asynchronous operation here
}));
// ol is disposed already, 'cause the asynchronous operation source task was awaited
// The next ObjectLockManager<AnyType>.Shared.LockAsync call will be processed now, if any
await ol.Task;// To throw any exception during performing the asynchronous operation

If AnyType implements the IObjectKey interface, it can be given to the ObjectLockManager<T> methods as object argument.

NOTE: ObjectLock will dispose itself as soon as RunTaskAsync has been called, and the given task was completed.

CLI arguments interpreter

There a just a few rules:

  1. A flag starts with a single dash
  2. A key for a value (list) starts with a double dash
  3. Keys/values can be quoted using single or double quotes
  4. Escape character is the backslash (only applicable in quoted values)
  5. A quoted value must be escaped for JSON decoding, a backslash must be double escaped
  6. Double quotes in a quoted value must be escaped always

Example:

"-flag" --key 'value1' value2 --key -value3 '--key2' "value"

For appending the value -value3 to the value list of key, the value needs to be added with another --key key identifier, 'cause it starts with a dash and could be misinterpreted as a flag (which would result in a parser error).

A CLI app called with these arguments could interpret them easy using the CliArguments class:

CliArguments cliArgs = new(args);
Assert.IsTrue(cliArgs["flag"]);
Assert.AreEqual(3, cliArgs.All("key").Count);
Assert.AreEqual("value", cliArgs.Single("key2"));

A -- (double dash) may be interpreted as an empty key name or a flag with the name -, based on if a value, which doesn't start with a dash, is following. Examples:

  • --: - flag
  • -- -: - flag (-- and - are both interpreted as double - flag (double flags will be combined))
  • -- value: Empty key with the value value
  • -- -key: - and key flags

Keyless arguments will be stored in the KeyLessArguments list - example:

CliArguments ca = CliArguments.Parse("value1 -flag value2 --key value3");
Assert.AreEqual(2, ca.KeyLessArguments.Count);
Assert.AreEqual("value1", ca.KeyLessArguments[0]);
Assert.AreEqual("value2", ca.KeyLessArguments[1]);
Assert.IsTrue(ca["flag"]);
Assert.IsTrue(ca["key", true]);
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 (14)

Showing the top 5 NuGet packages that depend on wan24-Core:

Package Downloads
Stream-Serializer-Extensions

Serializer extensions for .NET Stream objects.

wan24-Compression

Compression helper

wan24-Crypto

Crypto helper

wan24-Crypto-BC

Bouncy Castle adoption to wan24-Crypto

wan24-Compression-LZ4

LZ4 adoption for wan24-Compression

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
2.44.0 92 11/10/2024
2.43.0 88 11/3/2024
2.42.0 336 10/27/2024
2.41.0 76 10/21/2024
2.40.0 84 10/20/2024
2.39.0 97 9/29/2024
2.38.0 599 9/21/2024
2.37.0 114 9/15/2024
2.36.0 285 9/8/2024
2.35.0 227 8/24/2024
2.34.0 624 8/16/2024
2.33.0 230 8/4/2024
2.32.0 414 7/13/2024
2.31.0 390 7/6/2024
2.30.0 182 6/29/2024
2.29.0 349 6/22/2024
2.28.0 324 6/15/2024
2.27.0 97 6/8/2024
2.26.0 120 6/1/2024
2.25.0 127 5/26/2024
2.24.0 133 5/20/2024
2.23.0 172 5/11/2024
2.22.0 335 5/9/2024
2.21.0 139 5/5/2024
2.20.0 159 4/28/2024
2.19.0 157 4/20/2024
2.18.1 158 4/14/2024
2.18.0 344 4/12/2024
2.17.0 123 4/7/2024
2.16.0 215 3/30/2024
2.15.1 121 3/30/2024
2.15.0 112 3/30/2024
2.14.0 136 3/24/2024
2.13.0 148 3/17/2024
2.12.0 189 3/15/2024
2.11.0 153 3/10/2024
2.10.1 125 3/10/2024
2.10.0 229 3/9/2024
2.9.2 292 3/2/2024
2.9.1 132 3/2/2024
2.9.0 165 3/2/2024
2.8.0 138 2/25/2024
2.7.1 127 2/25/2024
2.7.0 111 2/25/2024
2.6.0 274 2/24/2024
2.5.0 117 2/20/2024
2.4.0 122 2/18/2024
2.3.2 186 2/17/2024
2.3.1 123 2/17/2024
2.3.0 121 2/17/2024
2.2.0 399 1/20/2024
2.1.0 125 12/23/2023
2.0.0 200 12/17/2023
1.43.0 161 11/27/2023
1.42.0 295 11/11/2023
1.41.2 119 11/4/2023
1.41.1 116 11/4/2023
1.41.0 118 11/4/2023
1.40.0 264 10/29/2023
1.39.0 276 10/21/2023
1.38.2 147 10/15/2023
1.38.1 317 10/14/2023
1.38.0 136 10/14/2023
1.37.0 138 10/13/2023
1.36.0 324 10/7/2023
1.35.0 212 10/1/2023
1.34.0 201 9/27/2023
1.33.0 124 9/20/2023
1.32.1 309 9/19/2023
1.32.0 114 9/19/2023
1.31.1 170 9/16/2023
1.31.0 185 9/16/2023
1.30.1 271 9/10/2023
1.30.0 136 9/10/2023
1.29.0 320 9/3/2023
1.28.0 141 8/26/2023
1.27.0 143 8/19/2023
1.26.0 163 8/5/2023
1.25.1 273 7/30/2023
1.25.0 263 7/30/2023
1.24.0 387 7/22/2023
1.23.0 136 7/9/2023
1.22.0 141 6/25/2023
1.21.0 162 6/24/2023
1.20.0 159 6/17/2023
1.19.0 204 6/11/2023
1.18.2 157 6/10/2023
1.18.1 157 6/9/2023
1.18.0 346 6/8/2023
1.17.0 144 6/4/2023
1.16.0 488 6/3/2023
1.15.0 317 5/29/2023
1.14.0 150 5/29/2023
1.13.0 152 5/28/2023
1.12.0 323 5/27/2023
1.11.0 143 5/24/2023
1.10.0 144 5/23/2023
1.9.0 133 5/22/2023
1.8.2 316 5/20/2023
1.8.1 156 5/20/2023
1.8.0 150 5/20/2023
1.7.1 171 5/13/2023
1.7.0 206 5/11/2023
1.6.1 1,519 4/26/2023
1.6.0 390 4/25/2023
1.5.0 418 4/22/2023
1.4.0 165 4/22/2023
1.3.0 303 4/16/2023
1.2.0 248 4/10/2023
1.1.0 188 4/7/2023
1.0.1 203 4/1/2023