Dekiru.Internals.ApiUtils 2.0.2

Prefix Reserved
dotnet add package Dekiru.Internals.ApiUtils --version 2.0.2
                    
NuGet\Install-Package Dekiru.Internals.ApiUtils -Version 2.0.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="Dekiru.Internals.ApiUtils" Version="2.0.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Dekiru.Internals.ApiUtils" Version="2.0.2" />
                    
Directory.Packages.props
<PackageReference Include="Dekiru.Internals.ApiUtils" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Dekiru.Internals.ApiUtils --version 2.0.2
                    
#r "nuget: Dekiru.Internals.ApiUtils, 2.0.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.
#:package Dekiru.Internals.ApiUtils@2.0.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Dekiru.Internals.ApiUtils&version=2.0.2
                    
Install as a Cake Addin
#tool nuget:?package=Dekiru.Internals.ApiUtils&version=2.0.2
                    
Install as a Cake Tool

Dekiru.ApiUtils

NuGet

A comprehensive ASP.NET Core utility library that provides standardised patterns for error handling, data access, row-level security, authentication, and more — so your team can focus on business logic instead of boilerplate.


Table of Contents


Installation

dotnet add package Dekiru.Internals.ApiUtils

Supported Frameworks

Framework Supported
.NET 8
.NET 9
.NET 10

Features

Error Handling & ProblemDetails

Registers a global exception handler that converts unhandled exceptions into RFC 7807 ProblemDetails responses.

Setup
// Program.cs
builder.Services.AddDefaultProblemDetailsProducer();

var app = builder.Build();
app.UseGlobalExceptionHandler(); // optionally pass showFullCallstack: true in Development
Throwing Typed Errors

ProblemDetailsException provides factory helpers for the most common HTTP error scenarios:

// 404 Not Found
throw ProblemDetailsException.NotFound("Order not found.");

// 400 Bad Request
throw ProblemDetailsException.BadRequest("Invalid date range.");

// 409 Conflict
throw ProblemDetailsException.Conflict("Duplicate entry.");

// 403 Forbidden
throw ProblemDetailsException.Forbidden();

// 410 Gone
throw ProblemDetailsException.Gone("Resource has been permanently removed.");

Attach arbitrary extensions for richer client-side error context:

throw ProblemDetailsException.NotFound("Product not found.")
    .WithExtensions(new Dictionary<string, object?> { ["productId"] = id });
Producing ProblemDetails Directly
return ProblemDetails.NotFound()
    .WithDetail("The requested resource could not be found.")
    .WithExtension("traceId", Activity.Current?.Id);
Custom Error Producer

Implement IProblemDetailsProducer and register it to override the default behaviour:

public class MyErrorProducer : IProblemDetailsProducer
{
    public ProblemDetails Create(HttpContext context, IExceptionHandlerPathFeature? ex, bool showFullCallstack)
    {
        // custom mapping logic
        return ProblemDetails.InternalServerError().WithDetail(ex?.Error.Message);
    }
}

builder.Services.AddCustomProblemDetailsProducer<MyErrorProducer>();

Generic Data Access (ContextService)

ContextServiceBase<TContext> wraps a DbContext and provides type-safe, async CRUD helpers with built-in pagination, predicate filtering, and automatic timestamp stamping.

Define a Service
public class AppContextService : ContextServiceBase<AppDbContext>
{
    public AppContextService(AppDbContext context, IServiceProvider provider)
        : base(context, provider) { }
}
Register It
builder.Services.AddContextService<AppContextService, AppDbContext>();
Common Operations
// Fetch a single entity by primary key
var order = await _service.Set<Order>()
    .FindAsync<Order, Guid>(orderId);

// Create
await _service.CreateAsync(new Order { ... });

// Update (auto-stamps IUpdated.Updated)
await _service.SaveChangesAsync();

// Soft-delete (sets Deleted = true if ISoftDeletable, otherwise hard-deletes)
await _service.DeleteAsync(order);
Request Types

!!! IMPORTANT !!! The request types have been moved to the Dekiru.ApiGenerator.Abstractions package.

Type Purpose
ListRequest<T> Filter, paginate, sort, and control EF tracking for list queries
GetRequest<T> Fetch a single entity by key (supports up to 3 composite keys)
CreateRequest<TPayload, TResult> Wrap a create payload
UpdateRequest<TKey, TPayload, TResult> Wrap an update payload with key(s)
DeleteRequest<TKey, TEntity> Wrap a delete by key(s)
CountRequest<T> Count matching entities

Domain Authorization (Row-Level Security)

DomainHandler<T> lets you declare per-entity read/update/delete predicates and create/update/delete authorization checks that are automatically enforced at the EF Core query filter level.

Define a Handler
public class OrderDomainHandler : DomainHandler<Order>
{
    private readonly ICurrentUser _user;

    public OrderDomainHandler(ICurrentUser user)
    {
        _user = user;
        ReadPredicate   = o => o.TenantId == _user.TenantId;
        UpdatePredicate = o => o.TenantId == _user.TenantId && !o.IsLocked;
        DeletePredicate = o => o.TenantId == _user.TenantId && o.IsDraft;
    }

    public override Task<AuthorizationResult> CheckCreateAsync(Order entity)
        => _user.HasPermission("orders.create") ? Pass() : Fail("Not allowed to create orders.");
}
Register & Apply Query Filters
// Register all handlers in DI
builder.Services.AddScoped<DomainHandler<Order>, OrderDomainHandler>();

// In your DbContext.OnModelCreating, apply global query filters
provider.GetRequiredService<DomainHandlerProvider>().RegisterQueryFilters(modelBuilder);
Passthrough Handler

Use PassthroughDomainHandler<T> for entities that need no restriction — it permits all operations and applies no filter.


Entity Interfaces

Implement these interfaces on your EF entities to opt in to automatic behaviour provided by ContextServiceBase.

public class Order : ICreated, IUpdated, ISoftDeletable, IRowVersioned
{
    public Guid Id { get; set; }

    // Auto-set on first SaveChangesAsync
    public DateTimeOffset Created { get; set; }

    // Auto-set on every SaveChangesAsync
    public DateTimeOffset Updated { get; set; }

    // DeleteAsync sets this to true instead of removing the row
    public bool Deleted { get; set; }

    // Concurrency token — use with RowVersion helpers
    public byte[] RowVersion { get; set; } = [];
}
Interface Behaviour
ICreated Created is stamped once on the first save
IUpdated Updated is stamped on every save
ISoftDeletable DeleteAsync sets Deleted = true rather than removing the row
IRowVersioned Enables optimistic concurrency via HasVersion() and RowVersion helpers

JSON & Service Configuration

A single extension method configures all recommended JSON serialiser settings:

builder.Services.ConfigureJson();
// Applies: camelCase property naming, ignore null values,
//          ignore reference cycles, string enum converter,
//          byte[] ↔ Base64 row-version converter

Register the row-version converter independently if needed:

builder.Services.AddRowVersionJsonConverter();

Azure Easy Auth

Parses the x-ms-client-principal header injected by Azure App Service / Container Apps Easy Auth into a standard ClaimsPrincipal.

builder.Services.AddEasyAuthAuthentication();

Use it the same way as any other ASP.NET Core authentication scheme — HttpContext.User is populated automatically. Extend EasyAuthHandler to customise claim transformation.


Row Versioning

Protect against lost-update conflicts with optimistic concurrency.

// In your EF model configuration
entity.Property(e => e.RowVersion).IsRowVersion();

// Validate incoming version from a client request
if (!entity.HasVersion(incomingVersion))
    throw ProblemDetailsException.Conflict("The resource has been modified. Please refresh and retry.");

// Convert between byte[] and Base64 string (e.g. for API payloads)
string base64 = RowVersion.ToString(entity.RowVersion);
byte[] bytes  = RowVersion.FromString(base64);

When AddRowVersionJsonConverter() (or ConfigureJson()) is registered, byte[] properties are automatically serialised as Base64 strings in all JSON responses.


SPA Middleware

Serve a Single Page Application with optional file-level content transformations (e.g. injecting environment variables into index.html at runtime).

app.UseSpaWithTransforms(
    contentPath: Path.Combine(Directory.GetCurrentDirectory(), "wwwroot"),
    config: (spa, values) =>
    {
        spa.Options.SourcePath = "ClientApp";
        // values can be used to substitute placeholders in static files
    });

Provide a StaticFileTransformerOptions instance for full control over caching headers, MIME types, and custom IFileProvider implementations.


License

MIT © Dekiru Solutions AB


LLM Quick Reference

This section is structured for AI code assistants. It provides a dense, scannable API surface so you can answer "how do I do X with this library?" without re-reading the full documentation above.

Library Mission

Dekiru.ApiUtils is a multi-targeted ASP.NET Core library (net8.0 / net9.0 / net10.0) that provides standardised, reusable infrastructure for REST APIs: RFC 7807 error responses, generic EF Core CRUD, row-level security, entity auditing, optimistic concurrency, Azure Easy Auth, JSON configuration, and SPA middleware.


Minimum Bootstrap (Program.cs)

// Error handling (required for ProblemDetailsException to produce HTTP responses)
builder.Services.AddDefaultProblemDetailsProducer();

// JSON (camelCase, null-ignore, cycle-safe, enum-as-string, row-version as Base64)
builder.Services.ConfigureJson();

// Domain authorization (row-level security via EF query filters)
builder.Services.AddDomainHandlers(); // scans calling assembly

// Context service (generic CRUD wrapper around DbContext)
builder.Services.AddContextService<AppContextService, AppDbContext>();

// Azure Easy Auth (optional — Azure App Service / Container Apps only)
builder.Services.AddEasyAuthAuthentication();

var app = builder.Build();
app.UseGlobalExceptionHandler(); // converts unhandled exceptions → ProblemDetails

Namespace Map

Namespace Contents
Dekiru.ApiUtils.ErrorHandling ProblemDetailsException, ProblemDetails, Maybe<T>, GlobalExceptionHandler, IProblemDetailsProducer, DefaultProblemDetailsProducer
Dekiru.ApiUtils.Services ContextServiceBase, ContextServiceBase<TContext>, all request types (ListRequest<T>, GetRequest, CreateRequest, UpdateRequest, DeleteRequest, CountRequest, LinkRequest, UnlinkRequest, ReplaceLinkRequest), Tracking enum
Dekiru.ApiUtils.Domain DomainHandler<T>, DomainHandlerProvider, PassthroughDomainHandler<T>, DomainHandlerExtensions
Dekiru.ApiUtils.Interfaces ICreated, IUpdated, ISoftDeletable, IRowVersioned
Dekiru.ApiUtils.Extensions ConfigurationExtensions, EntityFrameworkCoreExtensions, GeneralExtensions, SimpleValidationExtensions, CyclesExtensions
Dekiru.ApiUtils.Utils RowVersion, RowVersionJsonConverter, EasyAuthHandler, JsonProxy<T>, JsonProxyIgnoreAttribute, ValueSource
Dekiru.ApiUtils.Spa SpaStaticWithTransformsExtensions, StaticFileTransformerOptions, ValueSource

Scenario → API Reference

I want to… Use
Return a 404 from a controller/service throw ProblemDetailsException.NotFound("message")
Return a 400 Bad Request throw ProblemDetailsException.BadRequest("message")
Return a 409 Conflict throw ProblemDetailsException.Conflict("message")
Return a 403 Forbidden throw ProblemDetailsException.Forbidden()
Return a 410 Gone throw ProblemDetailsException.Gone("message")
Attach extra data to an error response .WithExtensions(new Dictionary<string, object?> { ["key"] = value })
Convert exception handling to ProblemDetails globally services.AddDefaultProblemDetailsProducer() + app.UseGlobalExceptionHandler()
Customise how exceptions map to responses Implement IProblemDetailsProducer, register with services.AddCustomProblemDetailsProducer<T>()
Wrap a result that may be an error Maybe<T> — use Map(), Bind(), Switch() for monadic chaining
Validate a required string value.NotNullOrWhiteSpace() (throws ProblemDetailsException 400 on failure)
Validate a numeric range value.InRange(min, max)
Validate enum membership value.InEnum<TEnum>()
Validate against a regex value.MatchRegex(pattern)
Fetch a list with pagination/filtering await _service.ListAsync(new ListRequest<T> { Skip = n, Take = n, Filter = x => ..., Sorting = [...] })
Fetch one entity by primary key await _service.Set<T>().FindAsync<T, TKey>(key)
Create an entity (with auto-timestamps) await _service.CreateAsync(entity)
Update an entity (auto-stamps IUpdated) mutate entity, then await _service.SaveChangesAsync()
Soft-delete or hard-delete an entity await _service.DeleteAsync(entity) — soft-deletes if ISoftDeletable, otherwise removes row
Count matching entities await _service.CountAsync(new CountRequest<T> { Filter = x => ... })
Apply row-level security to reads Define ReadPredicate in a DomainHandler<T> subclass; register with AddDomainHandlers()
Authorize create/update/delete in a handler Override CheckCreateAsync, CheckUpdateAsync, CheckDeleteAsync in DomainHandler<T>
Allow all operations (no restriction) Register / use PassthroughDomainHandler<T>
Apply EF query filters from domain handlers Call provider.GetRequiredService<DomainHandlerProvider>().RegisterQueryFilters(modelBuilder) in OnModelCreating
Detect optimistic concurrency conflict if (!entity.HasVersion(incomingBytes)) throw ProblemDetailsException.Conflict(...)
Serialize byte[] row version as Base64 builder.Services.AddRowVersionJsonConverter() (included in ConfigureJson())
Convert Base64 string ↔ byte[] manually RowVersion.ToString(bytes) / RowVersion.FromString(base64)
Parse Azure Easy Auth claims builder.Services.AddEasyAuthAuthentication() — populates HttpContext.User
Copy a JsonObject into a typed object new JsonProxy<T>(jsonObject).CopyTo(instance)
Serve SPA static files with runtime substitutions app.UseSpaWithTransforms(contentPath, (spa, values) => { ... })
Remove circular references from EF results .CleanAsync() / .CleanFirstOrDefault<T>() / .CleanSingleOrDefault<T>() extension methods
Link two entities by ID new LinkRequest<TSource, TTarget> { SourceId = ..., TargetId = ... }
Unlink two entities new UnlinkRequest<TSource, TTarget> { SourceId = ..., TargetId = ... }
Replace all links from one source to many targets new ReplaceLinkRequest<TSource, TTarget> { SourceId = ..., TargetIds = [...] }

Complete Public Type Catalog

Interfaces (Dekiru.ApiUtils.Interfaces)
Type Kind Purpose Key Member(s)
ICreated interface Auto-stamped creation timestamp DateTimeOffset Created { get; set; }
IUpdated interface Auto-stamped update timestamp DateTimeOffset Updated { get; set; }
ISoftDeletable interface Soft-delete flag bool Deleted { get; set; }
IRowVersioned interface Optimistic concurrency token byte[] RowVersion { get; set; }
Error Handling (Dekiru.ApiUtils.ErrorHandling)
Type Kind Purpose Key Member(s)
ProblemDetailsException exception class Throwable RFC 7807 error with HTTP status .NotFound(), .BadRequest(), .Conflict(), .Forbidden(), .Gone(), .WithExtensions()
ProblemDetails class RFC 7807 domain model; buildable .NotFound(), .BadRequest(), .WithDetail(), .WithExtension()
Maybe<T> class Success-or-error result monad .Map(), .Bind(), .Switch(), .GetValueOrDefault(), implicit operators
IProblemDetailsProducer interface Factory for custom error mapping ProblemDetails Create(HttpContext, IExceptionHandlerPathFeature?, bool)
DefaultProblemDetailsProducer class Default producer; virtual methods for customisation virtual ProblemDetails OnUnhandledException(...)
GlobalExceptionHandler static class Middleware registration helpers app.UseGlobalExceptionHandler(showFullCallstack?)
Services (Dekiru.ApiUtils.Services)
Type Kind Purpose Key Member(s)
ContextServiceBase abstract class Non-generic base; pre-save timestamp/soft-delete logic static PreSaveCheck(DbContext)
ContextServiceBase<TContext> abstract class Generic CRUD base wrapping TContext : DbContext Set<T>(), ListAsync(), CreateAsync(), DeleteAsync(), SaveChangesAsync(), BeginTransactionAsync()
Tracking enum EF query tracking mode Enabled, Disabled
RequestBase<T> abstract class Base for all query requests Filter, Predicate
ListRequest<T> class List with pagination, sort, filter, tracking, includes Skip, Take, Sorting, Filter, Tracking, Includes
GetRequest<TKey, T> class Fetch by single key Key, Tracking, Includes
GetRequest<TKey1, TKey2, T> class Fetch by two-part composite key Key1, Key2
GetRequest<TKey1, TKey2, TKey3, T> class Fetch by three-part composite key Key1, Key2, Key3
CreateRequest<TPayload, TResult> class Wrap a create payload Payload
UpdateRequest<TPayload, TResult> class Wrap update payload (key embedded in payload) Payload
UpdateRequest<TKey, TPayload, TResult> class Wrap update with separate key Key, Payload
UpdateRequest<TKey1, TKey2, TPayload, TResult> class Two-key update Key1, Key2, Payload
UpdateRequest<TKey1, TKey2, TKey3, TPayload, TResult> class Three-key update Key1, Key2, Key3, Payload
DeleteRequest<TKey, TEntity> class Delete by single key Key
DeleteRequest<TKey1, TKey2, TEntity> class Delete by two-part key Key1, Key2
DeleteRequest<TKey1, TKey2, TKey3, TEntity> class Delete by three-part key Key1, Key2, Key3
CountRequest<T> class Count entities matching optional filter Filter
LinkRequest<TSource, TTarget> class Link two entities (int IDs) SourceId, TargetId
LinkRequest<TSource, TTarget, TSourceKey, TTargetKey> class Link with generic key types SourceId, TargetId
UnlinkRequest<TSource, TTarget> class Unlink two entities (int IDs) SourceId, TargetId
UnlinkRequest<TSource, TTarget, TSourceKey, TTargetKey> class Unlink with generic keys SourceId, TargetId
ReplaceLinkRequest<TSource, TTarget> class Replace all target links from a source (int IDs) SourceId, TargetIds
ReplaceLinkRequest<TSource, TTarget, TSourceKey, TTargetKey> class Replace links with generic keys SourceId, TargetIds
Domain Authorization (Dekiru.ApiUtils.Domain)
Type Kind Purpose Key Member(s)
DomainHandler<T> abstract class Per-entity auth handler with predicates ReadPredicate, UpdatePredicate, DeletePredicate; CheckCreateAsync(), CheckUpdateAsync(), CheckDeleteAsync(), Pass(), Fail()
DomainHandlerProvider class Resolves handlers from DI; applies query filters RegisterQueryFilters(ModelBuilder), CheckCreateAsync<T>(), CheckUpdateAsync<T>(), CheckDeleteAsync<T>()
PassthroughDomainHandler<T> class No-op handler; allows all operations (no configuration needed)
Utils (Dekiru.ApiUtils.Utils)
Type Kind Purpose Key Member(s)
RowVersion static class byte[] ↔ Base64 conversions ToString(byte[]), FromString(string)
RowVersionJsonConverter class System.Text.Json converter for row version Registered via AddRowVersionJsonConverter()
EasyAuthHandler class ASP.NET Core auth handler for Azure Easy Auth Registered via AddEasyAuthAuthentication(); reads x-ms-client-principal header
JsonProxy<T> class Copies JsonObject properties to a typed instance CopyTo(T instance)
JsonProxyIgnoreAttribute attribute Excludes a property from JsonProxy<T> copying Apply to target type properties
ValueSource class Key-value store for SPA template substitution this[string key] indexer
SPA (Dekiru.ApiUtils.Spa)
Type Kind Purpose Key Member(s)
SpaStaticWithTransformsExtensions static class Middleware registration app.UseSpaWithTransforms(contentPath, config)
StaticFileTransformerOptions class Extends StaticFileOptions with transform config FileMatchPredicate, CacheControl

Entity Interface Decision Guide

Requirement Interface to implement
Record when a row was first created ICreated
Record when a row was last modified IUpdated
Hide deleted rows instead of removing them ISoftDeletable
Detect and reject stale concurrent updates IRowVersioned

All four can be combined on the same entity. ContextServiceBase handles stamping and deletion automatically during SaveChangesAsync and DeleteAsync.


Key Conventions

Convention Detail
DI Lifetimes ContextServiceBase<TContext>: scoped. DomainHandler<T>: scoped. DomainHandlerProvider: scoped.
Throwing errors Always use ProblemDetailsException factory methods. Never throw HttpRequestException or return raw status codes.
Timestamp stamping ContextServiceBase.PreSaveCheck() is called inside SaveChangesAsync. You do not call it manually.
Soft delete DeleteAsync checks ISoftDeletable; if implemented, sets Deleted = true and calls SaveChangesAsync. Hard-deletes otherwise.
JSON output After ConfigureJson(): property names are camelCase, null values are omitted, enums are strings, byte[] is Base64, object cycles are broken.
Row version on the wire Serialised as a Base64 string. Use RowVersion.FromString() to convert back before calling HasVersion().
Query filters Domain handler ReadPredicate is registered as a global EF Core query filter in OnModelCreating. It applies to every query on that entity type automatically.
Validation methods All SimpleValidationExtensions methods use [CallerArgumentExpression] to include the parameter name in the error message automatically.
Assembly scanning AddDomainHandlers() (no args) scans the calling assembly. Use AddDomainHandlers<T>() or AddDomainHandlers(Assembly) to target a specific assembly.

Component Wiring

The three main runtime components — DbContext, ContextServiceBase<TContext>, and DomainHandler<T> — connect as follows:

  1. Registration (Program.cs): Call AddContextService<TService, TContext>() and AddDomainHandlers(). Both are scoped to the HTTP request.

  2. Model creation (DbContext.OnModelCreating): Resolve DomainHandlerProvider from the service provider and call RegisterQueryFilters(modelBuilder). This iterates every registered DomainHandler<T> and attaches its ReadPredicate as an EF Core global query filter. The filter runs on every DbSet<T> query for the lifetime of the context.

  3. Request handling: Inject your ContextServiceBase<TContext> subclass. Call ListAsync, CreateAsync, DeleteAsync, etc. Before any write, ContextServiceBase internally:

    • Calls DomainHandlerProvider.CheckCreateAsync<T>() / CheckUpdateAsync<T>() / CheckDeleteAsync<T>() to enforce authorization.
    • Calls PreSaveCheck(context) to stamp timestamps and handle soft-delete flags.
  4. Error flow: If any authorization check or validation fails, a ProblemDetailsException is thrown. UseGlobalExceptionHandler() middleware catches it and writes a RFC 7807 JSON body with the correct HTTP status code.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  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.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.2 105 5/19/2026
2.0.1 121 4/10/2026
2.0.0 106 4/10/2026
2.0.0-preview.1 58 4/9/2026
2.0.0-preview.0 85 4/1/2026
1.5.1 743 12/2/2025
1.5.0 205 11/24/2025
1.5.0-rc.7 162 11/22/2025
1.5.0-rc.6 366 11/19/2025
1.5.0-rc.5 372 11/19/2025
1.5.0-rc.4 370 11/19/2025
1.5.0-rc.3 375 11/18/2025
1.5.0-rc.2 372 11/18/2025
1.5.0-rc.1 368 11/18/2025
1.5.0-rc.0 360 11/17/2025
1.4.1 299 11/11/2025
1.4.0 164 11/8/2025
1.3.9 212 11/5/2025
1.3.8 210 11/4/2025
1.3.7 211 11/4/2025
Loading failed