Doulex.DomainDriven.Repo.FileSystem 1.6.3

dotnet add package Doulex.DomainDriven.Repo.FileSystem --version 1.6.3
                    
NuGet\Install-Package Doulex.DomainDriven.Repo.FileSystem -Version 1.6.3
                    
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="Doulex.DomainDriven.Repo.FileSystem" Version="1.6.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Doulex.DomainDriven.Repo.FileSystem" Version="1.6.3" />
                    
Directory.Packages.props
<PackageReference Include="Doulex.DomainDriven.Repo.FileSystem" />
                    
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 Doulex.DomainDriven.Repo.FileSystem --version 1.6.3
                    
#r "nuget: Doulex.DomainDriven.Repo.FileSystem, 1.6.3"
                    
#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 Doulex.DomainDriven.Repo.FileSystem@1.6.3
                    
#: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=Doulex.DomainDriven.Repo.FileSystem&version=1.6.3
                    
Install as a Cake Addin
#tool nuget:?package=Doulex.DomainDriven.Repo.FileSystem&version=1.6.3
                    
Install as a Cake Tool

Doulex.DomainDriven

Build status CodeQL GitHub issues GitHub license

A comprehensive Domain-Driven Design (DDD) framework for .NET that provides essential building blocks for implementing clean architecture patterns with robust exception handling and Entity Framework Core integration.

โœจ Features

  • ๐Ÿ—๏ธ Complete DDD Building Blocks: Aggregate roots, entities, value objects, domain events, and repositories
  • ๐Ÿ”„ Unit of Work Pattern: Transaction management and change tracking
  • ๐Ÿ›ก๏ธ Comprehensive Exception Handling: Rich domain exceptions with automatic EF Core translation
  • ๐Ÿ“Š Entity Framework Core Integration: Ready-to-use repository implementations
  • ๐Ÿ”„ Intelligent Retry Logic: Automatic detection of retryable database operations
  • ๐Ÿ“ Structured Logging Support: Exception properties designed for monitoring and debugging
  • ๐ŸŽฏ Clean Architecture: Separation of concerns with clear boundaries
  • โšก High Performance: Minimal overhead with efficient implementations

๐Ÿ“ฆ NuGet Packages

Package Version Downloads Description
Doulex.DomainDriven nuget stats Core DDD abstractions and exception system
Doulex.DomainDriven.Repo.EFCore nuget stats Entity Framework Core implementation

๐Ÿš€ Quick Start

Installation

Install the core package:

dotnet add package Doulex.DomainDriven

For Entity Framework Core support:

dotnet add package Doulex.DomainDriven.Repo.EFCore

Or via Package Manager Console:

PM> Install-Package Doulex.DomainDriven
PM> Install-Package Doulex.DomainDriven.Repo.EFCore

Basic Usage

1. Define Your Domain Entities
// Define an aggregate root
public class User : AggregateRoot<int>
{
    public string Email { get; private set; }
    public string Name { get; private set; }

    public User(string email, string name)
    {
        Email = email;
        Name = name;
    }

    public void UpdateName(string newName)
    {
        if (string.IsNullOrWhiteSpace(newName))
            throw new ArgumentException("Name cannot be empty");

        Name = newName;
    }
}
2. Create Repository Interface
public interface IUserRepository : IRepository<User, int>
{
    Task<User?> GetByEmailAsync(string email, CancellationToken cancellationToken = default);
    Task<bool> EmailExistsAsync(string email, CancellationToken cancellationToken = default);
}
3. Implement Repository with EF Core
public class UserRepository : EntityFrameworkCoreRepository<User, int>, IUserRepository
{
    public UserRepository(DbContext context) : base(context)
    {
    }

    public async Task<User?> GetByEmailAsync(string email, CancellationToken cancellationToken = default)
    {
        return await GetAsync(u => u.Email == email, cancellationToken);
    }

    public async Task<bool> EmailExistsAsync(string email, CancellationToken cancellationToken = default)
    {
        return await ExistsAsync(u => u.Email == email, cancellationToken);
    }
}
4. Configure Services
// In Program.cs or Startup.cs
services.AddDbContext<YourDbContext>(options =>
    options.UseSqlServer(connectionString));

services.AddScoped<IUnitOfWork, EntityFrameworkCoreUnitOfWork<YourDbContext>>();
services.AddScoped<IUserRepository, UserRepository>();
5. Use in Application Services
public class UserService
{
    private readonly IUserRepository _userRepository;
    private readonly IUnitOfWork _unitOfWork;
    private readonly ILogger<UserService> _logger;

    public UserService(IUserRepository userRepository, IUnitOfWork unitOfWork, ILogger<UserService> logger)
    {
        _userRepository = userRepository;
        _unitOfWork = unitOfWork;
        _logger = logger;
    }

    public async Task<User> CreateUserAsync(string email, string name)
    {
        try
        {
            // Check if user already exists
            if (await _userRepository.EmailExistsAsync(email))
            {
                throw new ExistsException($"User with email {email} already exists");
            }

            // Create and save user
            var user = new User(email, name);
            await _userRepository.AddAsync(user);
            await _unitOfWork.SaveChangesAsync();

            _logger.LogInformation("User created successfully: {Email}", email);
            return user;
        }
        catch (RepoUpdateException ex)
        {
            _logger.LogError("Database update failed: {Message}", ex.Message);
            throw;
        }
    }
}

๐Ÿ›ก๏ธ Exception Handling

The framework provides comprehensive exception handling with automatic EF Core exception translation. All database exceptions are automatically converted to domain exceptions with rich context information:

Exception Types

Exception Description Use Case
RepoUpdateException Database update failures General database update operations
RepoUpdateConcurrencyException Optimistic concurrency conflicts Multiple users editing same data
RepoTimeoutException Operation timeouts Long-running queries
RepoTransactionException Transaction failures Commit/rollback issues
ExistsException Entity already exists Duplicate entity creation

Retry Logic Example

public async Task<bool> UpdateUserWithRetryAsync(User user, int maxRetries = 3)
{
    var retryCount = 0;

    while (retryCount < maxRetries)
    {
        try
        {
            await _userRepository.UpdateAsync(user);
            await _unitOfWork.SaveChangesAsync();
            return true;
        }
        catch (RepoUpdateConcurrencyException ex)
        {
            retryCount++;
            _logger.LogWarning("Concurrency conflict (attempt {Attempt}): {Message}",
                retryCount, ex.Message);

            if (retryCount >= maxRetries)
                throw;

            // Exponential backoff
            await Task.Delay(TimeSpan.FromMilliseconds(100 * Math.Pow(2, retryCount - 1)));
        }
        catch (RepoTimeoutException ex)
        {
            retryCount++;
            _logger.LogWarning("Timeout detected (attempt {Attempt}): {Message}",
                retryCount, ex.Message);

            if (retryCount >= maxRetries)
                throw;

            // Wait before retry
            await Task.Delay(TimeSpan.FromMilliseconds(500));
        }
    }

    return false;
}

๐Ÿ”„ Transaction Management

Basic Transaction Usage

public async Task TransferDataAsync(int fromUserId, int toUserId, decimal amount)
{
    ITransaction? transaction = null;

    try
    {
        transaction = await _unitOfWork.BeginTransactionAsync();

        // Perform multiple operations
        var fromUser = await _userRepository.GetAsync(fromUserId);
        var toUser = await _userRepository.GetAsync(toUserId);

        fromUser.DebitAmount(amount);
        toUser.CreditAmount(amount);

        await _userRepository.UpdateAsync(fromUser);
        await _userRepository.UpdateAsync(toUser);

        await _unitOfWork.SaveChangesAsync();
        await transaction.CommitAsync();

        _logger.LogInformation("Transfer completed: {Amount} from {FromUser} to {ToUser}",
            amount, fromUserId, toUserId);
    }
    catch (RepoTransactionException ex)
    {
        _logger.LogError("Transaction failed: {Operation} - {Message}",
            ex.FailedOperation, ex.Message);

        if (transaction != null)
        {
            await transaction.RollbackAsync();
        }

        throw;
    }
    finally
    {
        transaction?.Dispose();
    }
}

๐Ÿ—๏ธ Architecture Overview

The framework follows Domain-Driven Design principles with clean architecture:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    Application Layer                        โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                โ”‚
โ”‚  โ”‚  Application    โ”‚    โ”‚   Domain        โ”‚                โ”‚
โ”‚  โ”‚  Services       โ”‚    โ”‚   Services      โ”‚                โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                     Domain Layer                            โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                โ”‚
โ”‚  โ”‚  Aggregate      โ”‚    โ”‚   Domain        โ”‚                โ”‚
โ”‚  โ”‚  Roots          โ”‚    โ”‚   Events        โ”‚                โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                โ”‚
โ”‚  โ”‚  Entities       โ”‚    โ”‚   Value         โ”‚                โ”‚
โ”‚  โ”‚                 โ”‚    โ”‚   Objects       โ”‚                โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                  Infrastructure Layer                       โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                โ”‚
โ”‚  โ”‚  EF Core        โ”‚    โ”‚   Exception     โ”‚                โ”‚
โ”‚  โ”‚  Repositories   โ”‚    โ”‚   Translation   โ”‚                โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                โ”‚
โ”‚  โ”‚  Unit of Work   โ”‚    โ”‚   Database      โ”‚                โ”‚
โ”‚  โ”‚                 โ”‚    โ”‚   Context       โ”‚                โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๐Ÿ”ง Advanced Configuration

Custom Repository Implementation

public class CustomUserRepository : EntityFrameworkCoreRepository<User, int>, IUserRepository
{
    public CustomUserRepository(DbContext context) : base(context)
    {
    }

    public async Task<User[]> GetActiveUsersAsync(CancellationToken cancellationToken = default)
    {
        try
        {
            return await DbSet
                .Where(u => u.IsActive)
                .OrderBy(u => u.Name)
                .ToArrayAsync(cancellationToken);
        }
        catch (Exception ex)
        {
            // Automatic exception translation
            var translatedEx = ExceptionTranslator.TranslateException(ex, "GetActiveUsers");
            throw translatedEx;
        }
    }
}

Dependency Injection Setup

// Program.cs (ASP.NET Core)
var builder = WebApplication.CreateBuilder(args);

// Add Entity Framework
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// Add DDD services
builder.Services.AddScoped<IUnitOfWork, EntityFrameworkCoreUnitOfWork<ApplicationDbContext>>();
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<UserService>();

var app = builder.Build();

Exception Monitoring

public class ExceptionMonitoringService
{
    private readonly ILogger<ExceptionMonitoringService> _logger;

    public void LogDomainException(DomainDrivenException ex)
    {
        _logger.LogError("Domain Exception: {ErrorCode} - {Message} | Severity: {Severity} | Timestamp: {Timestamp}",
            ex.ErrorCode, ex.Message, ex.Severity, ex.Timestamp);

        // Send alerts for critical exceptions
        if (ex.Severity == ExceptionSeverity.Critical || ex.Severity == ExceptionSeverity.Fatal)
        {
            // Send notification to administrators
            SendAlert(ex);
        }
    }
}

๐Ÿ“Š Performance Considerations

  • Minimal Overhead: Exception translation only occurs when exceptions are thrown
  • Efficient Queries: Repository methods use optimized EF Core queries
  • Connection Pooling: Leverage EF Core's built-in connection pooling
  • Async Operations: All database operations are asynchronous
  • Memory Efficient: Context information uses existing EF Core data structures

๐Ÿงช Testing

Unit Testing Repositories

[Test]
public async Task GetAsync_WithValidId_ReturnsUser()
{
    // Arrange
    var options = new DbContextOptionsBuilder<TestDbContext>()
        .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
        .Options;

    await using var context = new TestDbContext(options);
    var repository = new UserRepository(context);

    var user = new User("test@example.com", "Test User");
    await repository.AddAsync(user);
    await context.SaveChangesAsync();

    // Act
    var result = await repository.GetAsync(user.Id);

    // Assert
    Assert.That(result, Is.Not.Null);
    Assert.That(result.Email, Is.EqualTo("test@example.com"));
}

Integration Testing with Exception Handling

[Test]
public async Task SaveChangesAsync_WithConcurrencyConflict_ThrowsConcurrencyException()
{
    // Arrange
    var user1 = await _repository.GetAsync(1);
    var user2 = await _repository.GetAsync(1);

    // Act & Assert
    user1.UpdateName("New Name 1");
    user2.UpdateName("New Name 2");

    await _repository.UpdateAsync(user1);
    await _unitOfWork.SaveChangesAsync(); // First update succeeds

    await _repository.UpdateAsync(user2);

    // Second update should throw concurrency exception
    Assert.ThrowsAsync<RepoUpdateConcurrencyException>(
        async () => await _unitOfWork.SaveChangesAsync());
}

๐Ÿš€ Migration Guide

From Raw EF Core

  1. Replace DbContext direct usage with Unit of Work pattern
  2. Implement repository interfaces for your aggregate roots
  3. Add exception handling around database operations
  4. Update dependency injection configuration

From Other DDD Frameworks

  1. Map existing entities to inherit from AggregateRoot<TKey>
  2. Implement repository interfaces using provided base classes
  3. Replace existing exception handling with domain exceptions
  4. Update service layer to use new repository interfaces

๐Ÿ“š Additional Resources

๐Ÿค Contributing

We welcome contributions! Please see our Contributing Guidelines for details.

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Add tests for new functionality
  5. Ensure all tests pass
  6. Submit a pull request

๐Ÿ“ Changelog

See CHANGELOG.md for a detailed history of changes.

โ“ Support

  • ๐Ÿ“– Documentation: Check the README and inline documentation
  • ๐Ÿ› Issues: Report bugs on GitHub Issues
  • ๐Ÿ’ฌ Discussions: Join discussions on GitHub Discussions
  • ๐Ÿ“ง Email: Contact the maintainers for enterprise support

๐Ÿ† Acknowledgments

  • Inspired by Domain-Driven Design principles by Eric Evans
  • Built on top of Entity Framework Core
  • Thanks to all contributors and the .NET community

๐Ÿ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.


โญ Star this repository if you find it helpful!

๐Ÿ”” Watch for updates and new releases.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  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.  net9.0 was computed.  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 was computed.  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. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen 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

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
1.6.3 154 9/3/2025
1.6.2 138 9/3/2025
1.6.1 153 6/5/2025
1.6.0 156 6/4/2025
1.5.2 159 3/27/2024
1.5.1 163 1/29/2024
1.4.1 262 11/17/2023
1.3.4 168 11/2/2023
1.3.2 154 11/2/2023
1.3.1 166 11/1/2023