Myth.Repository
4.3.0
dotnet add package Myth.Repository --version 4.3.0
NuGet\Install-Package Myth.Repository -Version 4.3.0
<PackageReference Include="Myth.Repository" Version="4.3.0" />
<PackageVersion Include="Myth.Repository" Version="4.3.0" />
<PackageReference Include="Myth.Repository" />
paket add Myth.Repository --version 4.3.0
#r "nuget: Myth.Repository, 4.3.0"
#:package Myth.Repository@4.3.0
#addin nuget:?package=Myth.Repository&version=4.3.0
#tool nuget:?package=Myth.Repository&version=4.3.0
<img style="float: right;" src="myth-repository-logo.png" alt="drawing" width="250"/>
Myth.Repository
Myth.Repository is a .NET library that provides a clean, standardized set of repository interfaces for data access. It promotes separation of concerns through read/write segregation, integrates seamlessly with the Specification pattern, and supports pagination out of the box. Perfect for building maintainable, testable, and domain-driven applications.
🎯 Why Myth.Repository?
Data access code is often the messiest part of applications. Controllers directly calling DbContext, business logic mixed with SQL, impossible to test without a real database, tight coupling to EF Core making migrations painful. Myth.Repository provides clean abstractions that decouple business logic from data access, enable true unit testing with mocks, support CQRS with read/write segregation, and work with the Specification pattern for reusable queries.
The Problem: Leaky Data Access
Controllers directly inject DbContext, business logic knows about EF Core, queries scattered everywhere, can't test without database, changing ORM requires rewriting entire app.
The Solution: Repository Pattern
Clean abstraction: IRepository<T> hides EF Core details. Read/Write segregation: IReadRepository for queries (CQRS queries), IWriteRepository for modifications (CQRS commands). Specification integration: Pass ISpec<T> for complex queries encapsulating business rules. Testable: Mock IRepository<T>, no database needed. Pagination built-in: IPaginated<T> results.
Key Benefits
Testability: Mock repositories in unit tests, no DbContext needed. CQRS-ready: Read/write segregation aligns perfectly with CQRS. DDD-aligned: Repositories are tactical DDD pattern for aggregate persistence. ORM-agnostic: Interfaces work with any implementation (EF Core, Dapper, Cosmos). Specification integration: Reusable, testable query logic.
Real-World Applications
E-Commerce: IProductRepository, IOrderRepository with specifications for complex filters (active products, orders by customer, price ranges). SaaS Multi-Tenant: Repositories automatically filter by tenant ID via specification. Event-Sourced Systems: Repository stores aggregates, encapsulates event storage logic.
Features
- Repository Interfaces: Standard contracts for data access operations
- Read/Write Segregation: Separate interfaces for read and write operations (CQRS-friendly)
- Specification Pattern Integration: First-class support for
ISpec<T>from Myth.Specification - Expression Support: Works with both specifications and LINQ expressions
- Pagination: Built-in pagination support with
IPaginated<T>results - Async-First: All operations are asynchronous with cancellation token support
- Query Operations: Rich querying capabilities (Where, First, Last, Any, All, Count)
- Batch Operations: Support for bulk inserts, updates, and deletes
- IQueryable Support: Access to underlying queryable for advanced scenarios
- IAsyncDisposable: Proper resource cleanup with async disposal
Installation
dotnet add package Myth.Repository
For Entity Framework Core implementation:
dotnet add package Myth.Repository.EntityFramework
Quick Start
Define Your Entity
public class Product {
public Guid Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public bool IsActive { get; set; }
public string Category { get; set; }
}
Create Repository Interface
using Myth.Interfaces.Repositories.Base;
public interface IProductRepository : IReadWriteRepositoryAsync<Product> {
// Add custom methods if needed
}
Implement Repository (Entity Framework Example)
using Myth.Repository.EntityFramework.Repositories;
public class ProductRepository : ReadWriteRepositoryAsync<Product>, IProductRepository {
public ProductRepository( DbContext context ) : base( context ) {
}
}
Use in Your Application
public class ProductService {
private readonly IProductRepository _repository;
public ProductService( IProductRepository repository ) {
_repository = repository;
}
public async Task<Product?> GetProductByIdAsync( Guid id, CancellationToken cancellationToken ) {
return await _repository.FirstOrDefaultAsync( p => p.Id == id, cancellationToken );
}
public async Task<IEnumerable<Product>> GetActiveProductsAsync( CancellationToken cancellationToken ) {
return await _repository.SearchAsync( p => p.IsActive, cancellationToken );
}
public async Task CreateProductAsync( Product product, CancellationToken cancellationToken ) {
await _repository.AddAsync( product, cancellationToken );
}
}
Core Interfaces
IRepository
Base marker interface for all repository types.
public interface IRepository { }
IReadRepositoryAsync<TEntity>
Provides read-only operations for querying data.
public interface IReadRepositoryAsync<TEntity> : IRepository, IAsyncDisposable {
// Queryable access
IQueryable<TEntity> Where( ISpec<TEntity> specification );
IQueryable<TEntity> Where( Expression<Func<TEntity, bool>> predicate );
IQueryable<TEntity> AsQueryable( );
IEnumerable<TEntity> AsEnumerable( );
// Search operations
Task<IEnumerable<TEntity>> SearchAsync( ISpec<TEntity> specification, CancellationToken cancellationToken = default );
Task<IEnumerable<TEntity>> SearchAsync( Expression<Func<TEntity, bool>> filterPredicate, Expression<Func<TEntity, bool>>? orderPredicate = null, CancellationToken cancellationToken = default );
// Paginated search
Task<IPaginated<TEntity>> SearchPaginatedAsync( ISpec<TEntity> specification, CancellationToken cancellationToken = default );
Task<IPaginated<TEntity>> SearchPaginatedAsync( Expression<Func<TEntity, bool>> filterPredicate, Pagination pagination, Expression<Func<TEntity, bool>>? orderPredicate = null, CancellationToken cancellationToken = default );
// Single item retrieval
Task<TEntity?> FirstOrDefaultAsync( ISpec<TEntity> specification, CancellationToken cancellationToken = default );
Task<TEntity?> FirstOrDefaultAsync( Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default );
Task<TEntity> FirstAsync( ISpec<TEntity> specification, CancellationToken cancellationToken = default );
Task<TEntity> FirstAsync( Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default );
Task<TEntity?> LastOrDefaultAsync( ISpec<TEntity> specification, CancellationToken cancellationToken = default );
Task<TEntity?> LastOrDefaultAsync( Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default );
Task<TEntity> LastAsync( ISpec<TEntity> specification, CancellationToken cancellationToken = default );
Task<TEntity> LastAsync( Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default );
// Aggregation operations
Task<int> CountAsync( ISpec<TEntity> specification, CancellationToken cancellationToken = default );
Task<int> CountAsync( Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default );
Task<bool> AnyAsync( ISpec<TEntity> specification, CancellationToken cancellationToken = default );
Task<bool> AnyAsync( Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default );
Task<bool> AllAsync( ISpec<TEntity> specification, CancellationToken cancellationToken = default );
Task<bool> AllAsync( Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default );
// Get all
Task<IEnumerable<TEntity>> ToListAsync( CancellationToken cancellationToken = default );
}
IWriteRepositoryAsync<TEntity>
Provides write operations for modifying data.
public interface IWriteRepositoryAsync<TEntity> : IRepository, IAsyncDisposable {
// Single operations
Task AddAsync( TEntity entity, CancellationToken cancellationToken = default );
Task UpdateAsync( TEntity entity, CancellationToken cancellationToken = default );
Task RemoveAsync( TEntity entity, CancellationToken cancellationToken = default );
// Batch operations
Task AddRangeAsync( IEnumerable<TEntity> entity, CancellationToken cancellationToken = default );
Task UpdateRangeAsync( IEnumerable<TEntity> entities, CancellationToken cancellationToken = default );
Task RemoveRangeAsync( IEnumerable<TEntity> entities, CancellationToken cancellationToken = default );
}
IReadWriteRepositoryAsync<TEntity>
Combines both read and write operations in a single interface.
public interface IReadWriteRepositoryAsync<TEntity> : IReadRepositoryAsync<TEntity>, IWriteRepositoryAsync<TEntity>, IAsyncDisposable { }
Pagination Support
IPaginated<T>
Interface representing paginated results:
public interface IPaginated<T> {
int PageNumber { get; } // Current page number (1-based)
int PageSize { get; } // Number of items per page
int TotalPages { get; } // Total number of pages
int TotalItems { get; } // Total number of items across all pages
IEnumerable<T> Items { get; } // Items in current page
}
Pagination Value Object
public class Pagination {
public int PageNumber { get; set; } // Default: 1
public int PageSize { get; set; } // Default: 10
// Static helpers
public static readonly Pagination Default = new( 1, 10 );
public static readonly Pagination All = new( -1, -1 );
}
Extension Method
using Myth.Extensions;
// Convert any IEnumerable to paginated result
var items = new List<Product> { /* ... */ };
var paginated = items.AsPaginated( take: 20, skip: 0 );
// Or with Pagination object
var pagination = new Pagination( pageNumber: 2, pageSize: 20 );
var paginated = items.AsPaginated( pagination );
Usage Examples
Basic CRUD Operations
public class ProductService {
private readonly IProductRepository _repository;
public ProductService( IProductRepository repository ) {
_repository = repository;
}
public async Task CreateAsync( Product product, CancellationToken ct ) {
await _repository.AddAsync( product, ct );
}
public async Task CreateManyAsync( IEnumerable<Product> products, CancellationToken ct ) {
await _repository.AddRangeAsync( products, ct );
}
public async Task UpdateAsync( Product product, CancellationToken ct ) {
await _repository.UpdateAsync( product, ct );
}
public async Task DeleteAsync( Product product, CancellationToken ct ) {
await _repository.RemoveAsync( product, ct );
}
}
Querying with Expressions
public class ProductService {
private readonly IProductRepository _repository;
public async Task<Product?> GetByIdAsync( Guid id, CancellationToken ct ) {
return await _repository.FirstOrDefaultAsync( p => p.Id == id, ct );
}
public async Task<IEnumerable<Product>> GetByCategoryAsync( string category, CancellationToken ct ) {
return await _repository.SearchAsync( p => p.Category == category, ct );
}
public async Task<int> CountActiveProductsAsync( CancellationToken ct ) {
return await _repository.CountAsync( p => p.IsActive, ct );
}
public async Task<bool> HasExpensiveProductsAsync( CancellationToken ct ) {
return await _repository.AnyAsync( p => p.Price > 1000, ct );
}
public async Task<bool> AreAllActiveAsync( CancellationToken ct ) {
return await _repository.AllAsync( p => p.IsActive, ct );
}
}
Querying with Specifications
using Myth.Specification;
// Define specifications
public static class ProductSpecifications {
public static ISpec<Product> IsActive( this ISpec<Product> spec ) {
return spec.And( p => p.IsActive );
}
public static ISpec<Product> InCategory( this ISpec<Product> spec, string category ) {
return spec.And( p => p.Category == category );
}
public static ISpec<Product> PriceRange( this ISpec<Product> spec, decimal min, decimal max ) {
return spec.And( p => p.Price >= min && p.Price <= max );
}
public static ISpec<Product> OrderByName( this ISpec<Product> spec ) {
return spec.Order( p => p.Name );
}
}
// Use in repository
public class ProductService {
private readonly IProductRepository _repository;
public async Task<IEnumerable<Product>> GetActiveProductsInCategoryAsync(
string category,
CancellationToken ct ) {
var spec = SpecBuilder<Product>.Create( )
.IsActive( )
.InCategory( category )
.OrderByName( );
return await _repository.SearchAsync( spec, ct );
}
public async Task<IEnumerable<Product>> GetAffordableProductsAsync(
decimal maxPrice,
CancellationToken ct ) {
var spec = SpecBuilder<Product>.Create( )
.IsActive( )
.PriceRange( 0, maxPrice )
.OrderByName( );
return await _repository.SearchAsync( spec, ct );
}
}
Pagination
public class ProductService {
private readonly IProductRepository _repository;
// Paginate with specification
public async Task<IPaginated<Product>> GetProductsPageAsync(
int pageNumber,
int pageSize,
CancellationToken ct ) {
var pagination = new Pagination( pageNumber, pageSize );
var spec = SpecBuilder<Product>.Create( )
.IsActive( )
.OrderByName( )
.Skip( (pageNumber - 1) * pageSize )
.Take( pageSize );
return await _repository.SearchPaginatedAsync( spec, ct );
}
// Paginate with expression
public async Task<IPaginated<Product>> GetProductsByCategoryPageAsync(
string category,
Pagination pagination,
CancellationToken ct ) {
return await _repository.SearchPaginatedAsync(
filterPredicate: p => p.Category == category && p.IsActive,
pagination: pagination,
orderPredicate: p => p.Name,
cancellationToken: ct );
}
// Use paginated results
public async Task DisplayProductsAsync( CancellationToken ct ) {
var result = await GetProductsPageAsync( pageNumber: 1, pageSize: 20, ct );
Console.WriteLine( $"Page {result.PageNumber} of {result.TotalPages}" );
Console.WriteLine( $"Total items: {result.TotalItems}" );
foreach ( var product in result.Items ) {
Console.WriteLine( $"- {product.Name}: ${product.Price}" );
}
}
}
Advanced Querying with IQueryable
public class ProductService {
private readonly IProductRepository _repository;
public async Task<IEnumerable<ProductSummary>> GetProductSummariesAsync( CancellationToken ct ) {
var queryable = _repository
.AsQueryable( )
.Where( p => p.IsActive )
.GroupBy( p => p.Category )
.Select( g => new ProductSummary {
Category = g.Key,
Count = g.Count( ),
AveragePrice = g.Average( p => p.Price )
} );
return await queryable.ToListAsync( ct );
}
}
Read/Write Segregation (CQRS)
// Read-only service
public class ProductQueryService {
private readonly IReadRepositoryAsync<Product> _repository;
public ProductQueryService( IReadRepositoryAsync<Product> repository ) {
_repository = repository;
}
public async Task<IEnumerable<Product>> GetAllAsync( CancellationToken ct ) {
return await _repository.ToListAsync( ct );
}
}
// Write-only service
public class ProductCommandService {
private readonly IWriteRepositoryAsync<Product> _repository;
public ProductCommandService( IWriteRepositoryAsync<Product> repository ) {
_repository = repository;
}
public async Task CreateAsync( Product product, CancellationToken ct ) {
await _repository.AddAsync( product, ct );
}
}
Integration with Other Myth Libraries
With Myth.Specification
var spec = SpecBuilder<Product>.Create( )
.IsActive( )
.InCategory( "Electronics" )
.PriceRange( 100, 500 )
.OrderByName( )
.Take( 50 );
var products = await _repository.SearchAsync( spec, cancellationToken );
With Myth.Flow.Actions (CQRS)
public class GetProductsQueryHandler : IQueryHandler<GetProductsQuery, IEnumerable<Product>> {
private readonly IReadRepositoryAsync<Product> _repository;
public async Task<QueryResult<IEnumerable<Product>>> HandleAsync(
GetProductsQuery query,
CancellationToken cancellationToken ) {
var products = await _repository.SearchAsync(
p => p.Category == query.Category,
cancellationToken );
return QueryResult<IEnumerable<Product>>.Success( products );
}
}
With Myth.Guard (Validation)
public class ProductCommandService {
private readonly IWriteRepositoryAsync<Product> _repository;
private readonly IValidator _validator;
public async Task CreateAsync( Product product, CancellationToken ct ) {
await _validator.ValidateAsync( product, ValidationContextKey.Create, ct );
await _repository.AddAsync( product, ct );
}
}
Architecture Patterns
Domain-Driven Design (DDD)
// Aggregate Root
public class Order : IAggregateRoot {
public Guid Id { get; private set; }
private List<OrderItem> _items = new( );
public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly( );
public void AddItem( Product product, int quantity ) {
_items.Add( new OrderItem( product, quantity ) );
}
}
// Repository in domain layer (interface)
public interface IOrderRepository : IReadWriteRepositoryAsync<Order> {
Task<Order?> GetByIdWithItemsAsync( Guid id, CancellationToken ct );
}
// Implementation in infrastructure layer
public class OrderRepository : ReadWriteRepositoryAsync<Order>, IOrderRepository {
public OrderRepository( DbContext context ) : base( context ) { }
public async Task<Order?> GetByIdWithItemsAsync( Guid id, CancellationToken ct ) {
return await AsQueryable( )
.Include( o => o.Items )
.FirstOrDefaultAsync( o => o.Id == id, ct );
}
}
Unit of Work Pattern
See Myth.Repository.EntityFramework for IUnitOfWorkRepository implementation.
public class OrderService {
private readonly IUnitOfWorkRepository _unitOfWork;
public async Task ProcessOrderAsync( Order order, CancellationToken ct ) {
await _unitOfWork.BeginTransactionAsync( ct );
try {
await _unitOfWork.AddAsync( order, ct );
await _unitOfWork.SaveChangesAsync( ct );
await _unitOfWork.CommitTransactionAsync( ct );
}
catch {
await _unitOfWork.RollbackTransactionAsync( ct );
throw;
}
}
}
Best Practices
- Use Specifications for Complex Queries: Encapsulate business rules in specification extensions
- Leverage Read/Write Segregation: Use separate interfaces when following CQRS patterns
- Always Use Cancellation Tokens: Support graceful cancellation in long-running operations
- Dispose Repositories Properly: Use
await usingor dependency injection for automatic disposal - Paginate Large Result Sets: Use
SearchPaginatedAsyncto avoid loading too much data - Keep Repositories Thin: Move business logic to services or domain entities
- Use IQueryable Sparingly: Prefer specifications and expressions for better testability
- Implement Custom Methods: Add domain-specific methods to repository interfaces when needed
Testing
public class ProductServiceTests {
private readonly Mock<IProductRepository> _mockRepository;
private readonly ProductService _service;
public ProductServiceTests( ) {
_mockRepository = new Mock<IProductRepository>( );
_service = new ProductService( _mockRepository.Object );
}
[Fact]
public async Task GetByIdAsync_ShouldReturnProduct_WhenExists( ) {
var productId = Guid.NewGuid( );
var product = new Product { Id = productId, Name = "Test" };
_mockRepository
.Setup( r => r.FirstOrDefaultAsync( It.IsAny<Expression<Func<Product, bool>>>( ), default ) )
.ReturnsAsync( product );
var result = await _service.GetByIdAsync( productId, default );
result.Should( ).NotBeNull( );
result.Id.Should( ).Be( productId );
}
}
Related Packages
- Myth.Repository.EntityFramework: Entity Framework Core implementation with Unit of Work support
- Myth.Specification: Build complex, reusable query specifications
- Myth.Commons: Value objects and pagination support (IPaginated<T>)
License
This project is licensed under the Apache License 2.0 - see the LICENSE for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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. |
-
net10.0
- Myth.Commons (>= 4.3.0)
- Myth.Specification (>= 4.3.0)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Myth.Repository:
| Package | Downloads |
|---|---|
|
Myth.Repository.EntityFramework
Entity Framework Core implementations of repository pattern with Unit of Work, specification support, expression handling, and transaction management for robust data access with EF Core. |
|
|
Packs.Template.BaseApi
Basis for any Packs API |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 4.3.0 | 29 | 2/1/2026 |
| 4.3.0-preview.3 | 30 | 2/1/2026 |
| 4.3.0-preview.2 | 142 | 12/22/2025 |
| 4.2.1-preview.1 | 617 | 12/2/2025 |
| 4.2.0 | 430 | 11/30/2025 |
| 4.2.0-preview.1 | 68 | 11/29/2025 |
| 4.1.0 | 346 | 11/27/2025 |
| 4.1.0-preview.3 | 135 | 11/27/2025 |
| 4.1.0-preview.2 | 128 | 11/27/2025 |
| 4.1.0-preview.1 | 135 | 11/26/2025 |
| 4.0.1 | 180 | 11/22/2025 |
| 4.0.1-preview.8 | 157 | 11/22/2025 |
| 4.0.1-preview.7 | 160 | 11/22/2025 |
| 4.0.1-preview.6 | 140 | 11/22/2025 |
| 4.0.1-preview.5 | 205 | 11/21/2025 |
| 4.0.1-preview.4 | 213 | 11/21/2025 |
| 4.0.1-preview.3 | 218 | 11/21/2025 |
| 4.0.1-preview.2 | 236 | 11/21/2025 |
| 4.0.1-preview.1 | 260 | 11/21/2025 |
| 4.0.0 | 408 | 11/20/2025 |
| 4.0.0-preview.3 | 347 | 11/19/2025 |
| 4.0.0-preview.2 | 97 | 11/15/2025 |
| 4.0.0-preview.1 | 125 | 11/15/2025 |
| 3.10.0 | 191 | 11/15/2025 |
| 3.0.5-preview.15 | 155 | 11/14/2025 |
| 3.0.5-preview.14 | 227 | 11/12/2025 |
| 3.0.5-preview.13 | 230 | 11/12/2025 |
| 3.0.5-preview.12 | 230 | 11/11/2025 |
| 3.0.5-preview.11 | 235 | 11/11/2025 |
| 3.0.5-preview.10 | 231 | 11/11/2025 |
| 3.0.5-preview.9 | 220 | 11/10/2025 |
| 3.0.5-preview.8 | 96 | 11/8/2025 |
| 3.0.5-preview.7 | 89 | 11/8/2025 |
| 3.0.5-preview.6 | 96 | 11/8/2025 |
| 3.0.5-preview.5 | 98 | 11/8/2025 |
| 3.0.5-preview.4 | 70 | 11/7/2025 |
| 3.0.5-preview.3 | 140 | 11/4/2025 |
| 3.0.5-preview.2 | 143 | 11/4/2025 |
| 3.0.5-preview.1 | 138 | 11/4/2025 |
| 3.0.4 | 238 | 11/3/2025 |
| 3.0.4-preview.19 | 92 | 11/2/2025 |
| 3.0.4-preview.17 | 83 | 11/1/2025 |
| 3.0.4-preview.16 | 85 | 11/1/2025 |
| 3.0.4-preview.15 | 82 | 10/31/2025 |
| 3.0.4-preview.14 | 140 | 10/31/2025 |
| 3.0.4-preview.13 | 143 | 10/30/2025 |
| 3.0.4-preview.12 | 135 | 10/23/2025 |
| 3.0.4-preview.11 | 133 | 10/23/2025 |
| 3.0.4-preview.10 | 125 | 10/23/2025 |
| 3.0.4-preview.9 | 126 | 10/23/2025 |
| 3.0.4-preview.8 | 132 | 10/22/2025 |
| 3.0.4-preview.6 | 126 | 10/21/2025 |
| 3.0.4-preview.5 | 135 | 10/21/2025 |
| 3.0.4-preview.4 | 126 | 10/20/2025 |
| 3.0.4-preview.3 | 129 | 10/20/2025 |
| 3.0.4-preview.2 | 55 | 10/18/2025 |
| 3.0.4-preview.1 | 131 | 10/7/2025 |
| 3.0.3 | 268 | 8/30/2025 |
| 3.0.2 | 159 | 8/23/2025 |
| 3.0.2-preview.4 | 136 | 8/21/2025 |
| 3.0.2-preview.3 | 106 | 8/16/2025 |
| 3.0.2-preview.1 | 89 | 5/23/2025 |
| 3.0.1 | 6,757 | 3/12/2025 |
| 3.0.1-preview.2 | 163 | 3/11/2025 |
| 3.0.1-preview.1 | 111 | 2/5/2025 |
| 3.0.0.2-preview | 172 | 12/10/2024 |
| 3.0.0.1-preview | 207 | 6/29/2024 |
| 3.0.0 | 4,710 | 12/10/2024 |
| 3.0.0-preview | 194 | 6/28/2024 |
| 2.0.0.17 | 2,270 | 4/17/2024 |
| 2.0.0.16 | 15,313 | 12/15/2023 |
| 2.0.0.15 | 279 | 12/15/2023 |
| 2.0.0.11 | 5,524 | 8/11/2022 |
| 2.0.0.10 | 2,784 | 7/20/2022 |
| 2.0.0.9 | 2,861 | 7/15/2022 |
| 2.0.0.8 | 2,895 | 7/12/2022 |
| 2.0.0.7 | 2,815 | 6/20/2022 |
| 2.0.0.6 | 2,916 | 5/23/2022 |
| 2.0.0.5 | 2,880 | 5/18/2022 |
| 2.0.0 | 2,991 | 2/17/2022 |