Marventa.Framework 5.2.0

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

🚀 Marventa.Framework

Enterprise .NET Framework - Convention over Configuration

NuGet License: MIT


📖 Table of Contents

  1. Installation
  2. Basic Setup (2 Lines!)
  3. Core - Domain Driven Design (DDD)
  4. Infrastructure - Database & Repository
  5. Behaviors - CQRS with MediatR
  6. Features - Caching
  7. Features - Event Bus
  8. Features - Storage
  9. Features - Search
  10. Features - Notification
  11. Features - Localization (i18n)
  12. Features - Template Engine
  13. Features - Reporting
  14. Features - Payment Gateway
  15. Features - Job Scheduler
  16. Features - Audit Logging
  17. Features - Logging
  18. Security - Authentication
  19. Security - Authorization
  20. Security - Rate Limiting
  21. Infrastructure - Multi-Tenancy
  22. Infrastructure - Health Checks
  23. Infrastructure - API Versioning
  24. Infrastructure - Swagger/OpenAPI
  25. Middleware - Exception Handling
  26. Enterprise Patterns
  27. Advanced Security
  28. Configuration - appsettings.json

1. Installation

dotnet add package Marventa.Framework

2. Basic Setup

Marventa Framework offers 4 flexible integration levels - choose what fits your needs:

Get started instantly with ALL features (Core + Extended):

var builder = WebApplication.CreateBuilder(args);

// ✨ ONE LINE - ALL features enabled (Core + Extended)
// Includes: CQRS, Repository, Event Sourcing, JWT, Caching, Event Bus, Outbox,
//           Notifications, Localization, Templating, Reporting, Payment, Job Scheduler, Audit Logging
builder.Services.AddMarventaFramework(builder.Configuration);

var app = builder.Build();

// ✨ ONE LINE - All middleware configured automatically
app.UseMarventa(builder.Configuration);

app.Run();

Use only architectural patterns and infrastructure - perfect for microservices:

var builder = WebApplication.CreateBuilder(args);

// ✨ CORE FEATURES ONLY - Perfect for microservices
// Includes: CQRS, Repository, Event Sourcing, JWT, Caching, Event Bus, Outbox,
//           Idempotency, Circuit Breaker, API Versioning, Swagger, Health Checks
builder.Services.AddMarventaCore(builder.Configuration);

var app = builder.Build();
app.UseMarventa(builder.Configuration);
app.Run();

🔧 Level 3: Core + Selected Extended Features

Mix and match - add only what you need:

var builder = WebApplication.CreateBuilder(args);

// Start with Core
builder.Services.AddMarventaCore(builder.Configuration);

// Add extended features selectively
builder.Services.AddNotifications(builder.Configuration);       // Email, SMS, Push
builder.Services.AddMarventaLocalization(builder.Configuration); // i18n
builder.Services.AddAuditLogging();                            // Compliance tracking

var app = builder.Build();
app.UseMarventa(builder.Configuration);
app.Run();

⚙️ Level 4: Granular Control (Advanced)

Full control with fluent API:

var builder = WebApplication.CreateBuilder(args);

// Fluent API for precise control
builder.Services.AddMarventaFramework(builder.Configuration, options =>
{
    // Core features
    options.AddCqrs();
    options.AddRepositoryPattern();
    options.AddEventBus();
    options.AddOutboxPattern();
    options.AddJwtAuthentication();
    options.AddCaching();

    // Extended features
    options.AddEmailService();
    options.AddSmsService();
    options.AddAuditLogging();
    options.AddTemplateEngine();
});

var app = builder.Build();
app.UseMarventa(builder.Configuration);
app.Run();

📊 Quick Comparison

Integration Level Use Case Features Included
Level 1: AddMarventaFramework() Monolith, Full-stack apps Core + Extended (Everything)
Level 2: AddMarventaCore() Microservices Core only (Patterns + Infrastructure)
Level 3: Core + Selective Hybrid, Custom needs Core + Selected extended features
Level 4: Fluent API Advanced, Fine-grained control Pick each feature individually

🔥 Feature Groupings

Core Features (Production-ready patterns):

  • ✅ CQRS & MediatR
  • ✅ Repository & Specification Pattern
  • ✅ Event Sourcing & Domain Events
  • ✅ Event Bus (RabbitMQ/Kafka/MassTransit)
  • ✅ Transactional Outbox Pattern
  • ✅ Idempotency
  • ✅ JWT + API Key Authentication
  • ✅ Redis Caching
  • ✅ Circuit Breaker & Retry (Polly)
  • ✅ API Versioning & Swagger
  • ✅ Health Checks
  • ✅ Multi-Tenancy
  • ✅ Rate Limiting

Extended Features (Optional enterprise services):

  • 📧 Notifications (Email, SMS, Push)
  • 🌍 Localization (i18n)
  • 📄 Template Engine (Liquid/Simple)
  • 📊 Reporting (PDF, Excel, CSV)
  • 💳 Payment Gateway (Stripe)
  • ⏰ Job Scheduler (Hangfire)
  • 📝 Audit Logging

🚀 Real-World Examples

E-commerce Monolith:

builder.Services.AddMarventaFramework(builder.Configuration);
// Get everything: CQRS, Events, Payments, Emails, Reporting, etc.

Order Microservice:

builder.Services.AddMarventaCore(builder.Configuration);
// Just patterns: CQRS, Repository, Event Bus, Outbox

Notification Microservice:

builder.Services.AddMarventaCore(builder.Configuration);
builder.Services.AddNotifications(builder.Configuration);
builder.Services.AddTemplateEngine(builder.Configuration);
// Core + Email/SMS/Push + Templates

Payment Microservice:

builder.Services.AddMarventaCore(builder.Configuration);
builder.Services.AddPaymentGateway(builder.Configuration);
builder.Services.AddAuditLogging();
// Core + Stripe + Audit

That's it! The framework automatically:

  • ✅ Registers controllers and JSON serialization
  • ✅ Scans assemblies for MediatR handlers, FluentValidation validators, and Mapster mappings
  • ✅ Configures middleware pipeline in correct order
  • ✅ Maps controller endpoints and health checks
  • ✅ Activates features based on appsettings.json

Advanced: Specify Assemblies to Scan

// Automatically scans calling assembly (recommended)
builder.Services.AddMarventa(builder.Configuration);

// Or explicitly specify assemblies to scan
builder.Services.AddMarventa(builder.Configuration, typeof(Program).Assembly);

// Or scan multiple assemblies
builder.Services.AddMarventa(
    builder.Configuration,
    typeof(Program).Assembly,
    typeof(SomeOtherClass).Assembly
);

3. Core - Domain Driven Design

3.1. Domain - Entity

Purpose: Represents domain objects with identity.

using Marventa.Framework.Core.Domain;

public class Product : Entity<Guid>
{
    public string Name { get; private set; }
    public decimal Price { get; private set; }
    public int Stock { get; private set; }

    private Product() { }

    public static Product Create(string name, decimal price, int stock)
    {
        return new Product
        {
            Id = Guid.NewGuid(),
            Name = name,
            Price = price,
            Stock = stock
        };
    }

    public void UpdateStock(int quantity)
    {
        Stock += quantity;
    }
}

3.2. Domain - Aggregate Root

Purpose: Root entity that manages business rules and dispatches domain events.

public class Order : AggregateRoot<Guid>
{
    private readonly List<OrderItem> _items = new();

    public string OrderNumber { get; private set; }
    public OrderStatus Status { get; private set; }
    public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();

    public static Order Create(string orderNumber)
    {
        var order = new Order
        {
            Id = Guid.NewGuid(),
            OrderNumber = orderNumber,
            Status = OrderStatus.Pending
        };

        order.AddDomainEvent(new OrderCreatedEvent(order.Id));
        return order;
    }

    public void Confirm()
    {
        Status = OrderStatus.Confirmed;
        AddDomainEvent(new OrderConfirmedEvent(Id));
    }
}

3.3. Domain - Value Object

Purpose: Objects without identity, compared by their values.

public class Address : ValueObject
{
    public string Street { get; private set; }
    public string City { get; private set; }
    public string ZipCode { get; private set; }

    public Address(string street, string city, string zipCode)
    {
        Street = street;
        City = city;
        ZipCode = zipCode;
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return Street;
        yield return City;
        yield return ZipCode;
    }
}

3.4. Domain - Domain Events

Purpose: Represents events that occur within the domain.

public record ProductCreatedEvent(Guid ProductId, string Name) : DomainEvent;
public record OrderCreatedEvent(Guid OrderId) : DomainEvent;
public record OrderConfirmedEvent(Guid OrderId) : DomainEvent;

3.5. Domain - Auditable Entity

Purpose: Automatically tracks creation and update information.

public class Customer : AuditableEntity<Guid>
{
    public string Name { get; set; }
    public string Email { get; set; }

    // CreatedAt, UpdatedAt, CreatedBy, UpdatedBy tracked automatically!
}

3.6. Application - Result Pattern

Purpose: Type-safe way to return success/failure states.

public async Task<Result<Guid>> CreateProduct(string name, decimal price)
{
    if (price <= 0)
        return Result<Guid>.Failure("Price must be positive");

    var product = Product.Create(name, price, 0);
    await _repository.AddAsync(product);

    return Result<Guid>.Success(product.Id);
}

4. Infrastructure - Database & Repository

4.1. Persistence - Create DbContext

Purpose: Database connection with Entity Framework Core.

using Marventa.Framework.Infrastructure.Persistence;

public class ApplicationDbContext : BaseDbContext
{
    public ApplicationDbContext(
        DbContextOptions<ApplicationDbContext> options,
        IHttpContextAccessor httpContextAccessor)
        : base(options, httpContextAccessor)
    {
    }

    public DbSet<Product> Products => Set<Product>();
    public DbSet<Order> Orders => Set<Order>();
}

Add to Program.cs:

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

builder.Services.AddScoped<IUnitOfWork>(sp =>
    new UnitOfWork(sp.GetRequiredService<ApplicationDbContext>()));

appsettings.json:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=MyDb;Trusted_Connection=true;"
  }
}

4.2. Persistence - Repository Pattern

Purpose: Abstracts database operations.

// Interface
public interface IProductRepository : IRepository<Product, Guid>
{
    Task<Product?> GetByNameAsync(string name);
    Task<List<Product>> SearchAsync(string searchTerm);
}

// Implementation
public class ProductRepository : GenericRepository<Product, Guid>, IProductRepository
{
    public ProductRepository(ApplicationDbContext context) : base(context)
    {
    }

    public async Task<Product?> GetByNameAsync(string name)
    {
        return await _dbSet.FirstOrDefaultAsync(p => p.Name == name);
    }

    public async Task<List<Product>> SearchAsync(string searchTerm)
    {
        return await _dbSet.Where(p => p.Name.Contains(searchTerm)).ToListAsync();
    }
}

Add to Program.cs:

builder.Services.AddScoped<IProductRepository, ProductRepository>();

4.3. Persistence - Unit of Work

Purpose: Manages transactions and dispatches domain events.

public class ProductService
{
    private readonly IProductRepository _repository;
    private readonly IUnitOfWork _unitOfWork;

    public async Task<Result<Guid>> CreateProductAsync(string name, decimal price)
    {
        var product = Product.Create(name, price, 0);
        await _repository.AddAsync(product);

        // Transaction + Domain Events
        await _unitOfWork.SaveChangesAsync();

        return Result<Guid>.Success(product.Id);
    }
}

4.4. Persistence - Data Seeding

Purpose: Seed initial data into database with helper infrastructure.

// Create a seeder
public class UserSeeder : DataSeederBase<ApplicationDbContext>
{
    public UserSeeder(ApplicationDbContext context) : base(context)
    {
    }

    public override int Order => 1; // Execution order

    public override async Task SeedAsync(CancellationToken cancellationToken = default)
    {
        if (await AnyAsync<User>(cancellationToken))
            return;

        var users = new List<User>
        {
            User.Create("admin@example.com", "Admin User"),
            User.Create("user@example.com", "Regular User")
        };

        await AddRangeAsync(users, cancellationToken);
    }
}

Register Seeders:

builder.Services.AddScoped<IDataSeeder, UserSeeder>();
builder.Services.AddScoped<IDataSeeder, ProductSeeder>();

Run Seeders:

// In Program.cs after app.Build()
using (var scope = app.Services.CreateScope())
{
    var seederRunner = scope.ServiceProvider.GetRequiredService<DataSeederRunner>();
    await seederRunner.RunAsync();
}

4.5. Persistence - MongoDB Repository Pattern

Purpose: Complete repository pattern implementation for MongoDB with document-based entities.

Install MongoDB Package:

dotnet add package MongoDB.Driver

Configuration (appsettings.json):

{
  "MongoDB": {
    "ConnectionString": "mongodb://localhost:27017",
    "DatabaseName": "MyDatabase"
  }
}

Create MongoDB Entity:

using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using Marventa.Framework.Core.Domain;

public class Product : MongoEntity<ObjectId>
{
    [BsonElement("name")]
    public string Name { get; set; }

    [BsonElement("price")]
    public decimal Price { get; set; }

    [BsonElement("stock")]
    public int Stock { get; set; }

    [BsonElement("category")]
    public string Category { get; set; }

    [BsonElement("isActive")]
    public bool IsActive { get; set; }

    public static Product Create(string name, decimal price, int stock, string category)
    {
        return new Product
        {
            Id = ObjectId.GenerateNewId(),
            Name = name,
            Price = price,
            Stock = stock,
            Category = category,
            IsActive = true
        };
    }
}

Setup in Program.cs:

// Register MongoDB context and services
builder.Services.AddMongoDb(builder.Configuration);

// Register repository
builder.Services.AddScoped<IMongoRepository<Product, ObjectId>, MongoRepository<Product, ObjectId>>();

Create Repository Interface:

public interface IProductRepository : IMongoRepository<Product, ObjectId>
{
    Task<List<Product>> GetActiveProductsAsync();
    Task<List<Product>> SearchByNameAsync(string searchTerm);
    Task<List<Product>> GetByCategoryAsync(string category);
}

Create Repository Implementation:

using Marventa.Framework.Infrastructure.Persistence.MongoDB;

public class ProductRepository : MongoRepository<Product, ObjectId>, IProductRepository
{
    public ProductRepository(IMongoDatabase database) : base(database)
    {
    }

    public async Task<List<Product>> GetActiveProductsAsync()
    {
        return await FindAsync(p => p.IsActive);
    }

    public async Task<List<Product>> SearchByNameAsync(string searchTerm)
    {
        return await FindAsync(p => p.Name.Contains(searchTerm));
    }

    public async Task<List<Product>> GetByCategoryAsync(string category)
    {
        return await FindAsync(p => p.Category == category && p.IsActive);
    }
}

Usage in Service:

public class ProductService
{
    private readonly IProductRepository _repository;

    public ProductService(IProductRepository repository)
    {
        _repository = repository;
    }

    public async Task<ObjectId> CreateProductAsync(string name, decimal price, int stock, string category)
    {
        var product = Product.Create(name, price, stock, category);
        await _repository.AddAsync(product);
        return product.Id;
    }

    public async Task<Product?> GetProductAsync(ObjectId id)
    {
        return await _repository.GetByIdAsync(id);
    }

    public async Task<List<Product>> GetAllProductsAsync()
    {
        return await _repository.GetAllAsync();
    }

    public async Task<List<Product>> SearchProductsAsync(string searchTerm)
    {
        return await _repository.SearchByNameAsync(searchTerm);
    }

    public async Task UpdateProductAsync(ObjectId id, decimal newPrice, int newStock)
    {
        var product = await _repository.GetByIdAsync(id);
        if (product != null)
        {
            product.Price = newPrice;
            product.Stock = newStock;
            await _repository.UpdateAsync(product);
        }
    }

    public async Task DeleteProductAsync(ObjectId id)
    {
        await _repository.DeleteAsync(id);
    }

    public async Task<long> CountActiveProductsAsync()
    {
        return await _repository.CountAsync(p => p.IsActive);
    }

    public async Task<bool> ProductExistsAsync(string name)
    {
        return await _repository.ExistsAsync(p => p.Name == name);
    }
}

Usage in Controller:

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly ProductService _productService;

    public ProductsController(ProductService productService)
    {
        _productService = productService;
    }

    [HttpPost]
    public async Task<IActionResult> Create([FromBody] CreateProductRequest request)
    {
        var id = await _productService.CreateProductAsync(
            request.Name,
            request.Price,
            request.Stock,
            request.Category);

        return Ok(new { id = id.ToString() });
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> Get(string id)
    {
        if (!ObjectId.TryParse(id, out var objectId))
            return BadRequest("Invalid ID format");

        var product = await _productService.GetProductAsync(objectId);
        return product != null ? Ok(product) : NotFound();
    }

    [HttpGet]
    public async Task<IActionResult> GetAll()
    {
        var products = await _productService.GetAllProductsAsync();
        return Ok(products);
    }

    [HttpGet("search")]
    public async Task<IActionResult> Search([FromQuery] string term)
    {
        var products = await _productService.SearchProductsAsync(term);
        return Ok(products);
    }

    [HttpPut("{id}")]
    public async Task<IActionResult> Update(string id, [FromBody] UpdateProductRequest request)
    {
        if (!ObjectId.TryParse(id, out var objectId))
            return BadRequest("Invalid ID format");

        await _productService.UpdateProductAsync(objectId, request.Price, request.Stock);
        return NoContent();
    }

    [HttpDelete("{id}")]
    public async Task<IActionResult> Delete(string id)
    {
        if (!ObjectId.TryParse(id, out var objectId))
            return BadRequest("Invalid ID format");

        await _productService.DeleteProductAsync(objectId);
        return NoContent();
    }
}

Available Repository Methods:

  • GetByIdAsync(id) - Get single document by ID
  • GetAllAsync() - Get all documents
  • FindAsync(predicate) - Find documents by expression
  • AddAsync(entity) - Insert single document
  • AddRangeAsync(entities) - Insert multiple documents
  • UpdateAsync(entity) - Update document
  • DeleteAsync(id) - Delete by ID
  • DeleteAsync(entity) - Delete document
  • CountAsync(predicate) - Count documents
  • ExistsAsync(predicate) - Check if document exists
  • GetPagedAsync(pageNumber, pageSize, predicate) - Paginated results

Benefits:

  • ✅ No DbContext needed - lightweight and fast
  • ✅ BSON ObjectId support out of the box
  • ✅ Async/await throughout
  • ✅ Expression-based queries
  • ✅ Full CRUD operations
  • ✅ Pagination support
  • ✅ Integration with Marventa.Framework patterns

5. Behaviors - CQRS with MediatR

Purpose: MediatR is auto-registered with validation/logging/performance behaviors active.

5.1. Create Command

// Command
public record CreateProductCommand(string Name, decimal Price) : IRequest<Result<Guid>>;

// Handler
public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, Result<Guid>>
{
    private readonly IProductRepository _repository;
    private readonly IUnitOfWork _unitOfWork;

    public async Task<Result<Guid>> Handle(CreateProductCommand request, CancellationToken ct)
    {
        var product = Product.Create(request.Name, request.Price, 0);
        await _repository.AddAsync(product);
        await _unitOfWork.SaveChangesAsync(ct);

        return Result<Guid>.Success(product.Id);
    }
}

// Validator
public class CreateProductCommandValidator : AbstractValidator<CreateProductCommand>
{
    public CreateProductCommandValidator()
    {
        RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
        RuleFor(x => x.Price).GreaterThan(0);
    }
}

5.2. Create Query

// Query
public record GetProductByIdQuery(Guid Id) : IRequest<Result<ProductDto>>;

// Handler
public class GetProductByIdQueryHandler : IRequestHandler<GetProductByIdQuery, Result<ProductDto>>
{
    private readonly IProductRepository _repository;

    public async Task<Result<ProductDto>> Handle(GetProductByIdQuery request, CancellationToken ct)
    {
        var product = await _repository.GetByIdAsync(request.Id);
        if (product == null)
            return Result<ProductDto>.Failure("Product not found");

        return Result<ProductDto>.Success(new ProductDto(product.Id, product.Name, product.Price));
    }
}

public record ProductDto(Guid Id, string Name, decimal Price);

Usage in Controller:

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IMediator _mediator;

    [HttpPost]
    public async Task<IActionResult> Create([FromBody] CreateProductCommand command)
    {
        var result = await _mediator.Send(command);
        return result.IsSuccess ? Ok(result.Value) : BadRequest(result.ErrorMessage);
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetById(Guid id)
    {
        var result = await _mediator.Send(new GetProductByIdQuery(id));
        return result.IsSuccess ? Ok(result.Value) : NotFound(result.ErrorMessage);
    }
}

5.3. Validation Behavior

Purpose: Automatically validates all requests with FluentValidation. Auto-active! Just write validator classes.

5.4. Logging Behavior

Purpose: Logs all requests/responses. Auto-active!

5.5. Performance Behavior

Purpose: Warns about requests taking longer than 500ms. Auto-active!


6. Features - Caching

Purpose: Framework supports three caching strategies: InMemory, Redis, and Hybrid (two-level cache).

6.1. InMemory Cache

Configuration (appsettings.json):

{
  "MemoryCache": {
    "SizeLimit": 1024,
    "CompactionPercentage": 0.25,
    "ExpirationScanFrequency": "00:01:00"
  }
}

Usage:

using Marventa.Framework.Features.Caching.Abstractions;

public class ProductService
{
    private readonly ICacheService _cache;

    public async Task<Product?> GetProductAsync(Guid id)
    {
        var cacheKey = $"product:{id}";

        // Try get from cache
        var cached = await _cache.GetAsync<Product>(cacheKey);
        if (cached != null) return cached;

        // Get from database
        var product = await _repository.GetByIdAsync(id);

        // Set cache with expiration
        await _cache.SetAsync(cacheKey, product, TimeSpan.FromHours(1));

        return product;
    }

    public async Task RemoveProductCacheAsync(Guid id)
    {
        await _cache.RemoveAsync($"product:{id}");
    }
}

6.2. Output Cache

Purpose: ASP.NET Core 7+ output caching for HTTP responses.

Configuration:

{
  "OutputCache": {
    "Enabled": true,
    "DefaultExpirationSeconds": 60,
    "VaryByQuery": true,
    "VaryByHeader": false,
    "VaryByHeaderNames": []
  }
}

Add to Program.cs:

builder.Services.AddMarventaOutputCache(builder.Configuration);

Usage in Controllers:

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    // Cache response for 60 seconds (from configuration)
    [OutputCache]
    [HttpGet]
    public async Task<IActionResult> GetAll()
    {
        var products = await _mediator.Send(new GetAllProductsQuery());
        return Ok(products);
    }

    // Custom cache duration
    [OutputCache(Duration = 300)]
    [HttpGet("{id}")]
    public async Task<IActionResult> GetById(Guid id)
    {
        var product = await _mediator.Send(new GetProductByIdQuery(id));
        return Ok(product);
    }
}

6.3. Redis Cache

Configuration (appsettings.json):

{
  "Caching": { "Type": "Redis" },
  "Redis": {
    "ConnectionString": "localhost:6379",
    "InstanceName": "MyApp:"
  }
}

Usage: Same interface as InMemory (ICacheService). Framework automatically switches based on configuration.

6.4. Hybrid Cache

Purpose: Two-level caching - reads from InMemory (L1) first, then Redis (L2). Best of both worlds!

Configuration:

{
  "Caching": { "Type": "Hybrid" },
  "MemoryCache": {
    "SizeLimit": 1024,
    "CompactionPercentage": 0.25
  },
  "Redis": {
    "ConnectionString": "localhost:6379",
    "InstanceName": "MyApp:"
  }
}

How it works:

  • Get: Checks InMemory first, then Redis if not found
  • Set: Writes to both InMemory and Redis
  • Remove: Removes from both caches

Usage: Same ICacheService interface - completely transparent!

6.5. Modular Caching Setup

Add specific cache type:

// Add InMemory cache only
builder.Services.AddInMemoryCaching(builder.Configuration);

// Add Redis cache only
builder.Services.AddRedisCaching(builder.Configuration);

// Add Hybrid cache
builder.Services.AddHybridCaching(builder.Configuration);

// Auto-detect from configuration (used by AddMarventa)
builder.Services.AddMarventaCaching(builder.Configuration);

7. Features - Event Bus

7.1. RabbitMQ Event Bus

Configuration:

{
  "RabbitMQ": {
    "Host": "localhost",
    "Username": "guest",
    "Password": "guest"
  }
}

Publish:

public class OrderCreatedEvent : IntegrationEvent
{
    public Guid OrderId { get; }
    public string OrderNumber { get; }

    public OrderCreatedEvent(Guid orderId, string orderNumber)
    {
        OrderId = orderId;
        OrderNumber = orderNumber;
    }
}

await _eventBus.PublishAsync(new OrderCreatedEvent(orderId, orderNumber));

Subscribe:

public class OrderCreatedEventHandler : IIntegrationEventHandler<OrderCreatedEvent>
{
    public async Task HandleAsync(OrderCreatedEvent @event)
    {
        Console.WriteLine($"Order created: {@event.OrderNumber}");
    }
}

// Add to Program.cs
builder.Services.AddScoped<IIntegrationEventHandler<OrderCreatedEvent>, OrderCreatedEventHandler>();

7.2. Kafka Producer/Consumer

Configuration:

{
  "Kafka": {
    "BootstrapServers": "localhost:9092",
    "GroupId": "myapp-group"
  }
}

Usage:

// Produce
await _kafkaProducer.ProduceAsync("my-topic", new { UserId = 123 });

// Consume
await _kafkaConsumer.ConsumeAsync("my-topic", async message =>
{
    Console.WriteLine($"Received: {message}");
});

7.3. MassTransit Integration

Configuration:

{
  "MassTransit": { "Enabled": "true" },
  "RabbitMQ": {
    "Host": "localhost",
    "Username": "guest",
    "Password": "guest"
  }
}

Usage:

// Consumer
public class OrderCreatedConsumer : IConsumer<OrderCreated>
{
    public async Task Consume(ConsumeContext<OrderCreated> context)
    {
        Console.WriteLine($"Order {context.Message.OrderId}");
    }
}

// Publish
await _publishEndpoint.Publish(new OrderCreated { OrderId = 123 });

8. Features - Storage

8.1. Local File Storage

Configuration:

{
  "LocalStorage": {
    "BasePath": "D:/uploads",
    "BaseUrl": "https://myapp.com/files"
  }
}

Usage:

// Upload
await _storage.UploadAsync(fileStream, "documents/file.pdf");

// Download
var stream = await _storage.DownloadAsync("documents/file.pdf");

// Delete
await _storage.DeleteAsync("documents/file.pdf");

// Get URL
var url = await _storage.GetUrlAsync("documents/file.pdf");

8.2. Azure Blob Storage

Configuration:

{
  "Azure": {
    "Storage": {
      "ConnectionString": "your-connection-string",
      "ContainerName": "uploads"
    }
  }
}

Usage: Same as Local Storage.

8.3. AWS S3 Storage

Configuration:

{
  "AWS": {
    "AccessKey": "your-key",
    "SecretKey": "your-secret",
    "Region": "us-east-1",
    "BucketName": "my-bucket"
  }
}

Usage: Same as Local Storage.


9.1. Elasticsearch

Configuration:

{
  "Elasticsearch": {
    "Uri": "http://localhost:9200"
  }
}

Usage:

// Index
await _elasticsearchService.IndexAsync("products", product);

// Search
var results = await _elasticsearchService.SearchAsync<Product>("products", "laptop");

10. Features - Notification

Purpose: Send notifications via Email, SMS, and Push Notifications with multiple provider support.

10.1. Email Services

Supported Providers:

  • SMTP - Standard SMTP with MailKit
  • SendGrid - Cloud-based email delivery
  • AWS SES - Amazon Simple Email Service (placeholder)

Configuration (appsettings.json):

{
  "Notification": {
    "EmailProvider": "Smtp",
    "EnableEmail": true
  },
  "Smtp": {
    "Host": "smtp.gmail.com",
    "Port": 587,
    "EnableSsl": true,
    "Username": "your-email@gmail.com",
    "Password": "your-password",
    "FromEmail": "noreply@yourapp.com",
    "FromName": "Your App",
    "TimeoutSeconds": 30
  }
}

Setup:

// Program.cs - Automatic registration based on configuration
builder.Services.AddNotifications(builder.Configuration);

// Or register specific email provider
builder.Services.AddSmtpEmail(builder.Configuration);
builder.Services.AddSendGridEmail(builder.Configuration);

Usage:

using Marventa.Framework.Features.Notification.Abstractions;
using Marventa.Framework.Features.Notification.Models;

public class UserService
{
    private readonly IEmailService _emailService;

    // Simple email
    public async Task SendWelcomeEmailAsync(string email, string name)
    {
        var result = await _emailService.SendSimpleAsync(
            to: email,
            subject: "Welcome to Our App!",
            body: $"<h1>Hello {name}!</h1><p>Welcome aboard!</p>",
            isHtml: true
        );

        if (result.IsSuccess)
        {
            Console.WriteLine($"Email sent: {result.MessageId}");
        }
    }

    // Advanced email with attachments
    public async Task SendInvoiceEmailAsync(string email, byte[] pdfContent)
    {
        var message = new EmailMessage
        {
            Subject = "Your Invoice",
            HtmlBody = "<p>Please find your invoice attached.</p>",
            Priority = EmailPriority.High
        };

        message
            .AddTo(email)
            .AddCc("accounting@company.com")
            .AddAttachment("invoice.pdf", pdfContent, "application/pdf");

        var result = await _emailService.SendAsync(message);
    }
}

SendGrid Configuration:

{
  "Notification": {
    "EmailProvider": "SendGrid",
    "EnableEmail": true
  },
  "SendGrid": {
    "ApiKey": "SG.your-api-key",
    "FromEmail": "noreply@yourapp.com",
    "FromName": "Your App",
    "SandboxMode": false
  }
}

10.2. SMS Services

Supported Providers:

  • Twilio - Popular SMS service with global coverage
  • Vonage (Nexmo) - Cloud communications platform

Configuration (Twilio):

{
  "Notification": {
    "SmsProvider": "Twilio",
    "EnableSms": true
  },
  "Twilio": {
    "AccountSid": "your-account-sid",
    "AuthToken": "your-auth-token",
    "FromPhoneNumber": "+1234567890",
    "MessagingServiceSid": "optional-messaging-service-sid"
  }
}

Setup:

// Program.cs
builder.Services.AddNotifications(builder.Configuration);

// Or register specific SMS provider
builder.Services.AddTwilioSms(builder.Configuration);
builder.Services.AddVonageSms(builder.Configuration);

Usage:

using Marventa.Framework.Features.Notification.Abstractions;
using Marventa.Framework.Features.Notification.Models;

public class AuthService
{
    private readonly ISmsService _smsService;

    // Simple SMS
    public async Task SendOtpAsync(string phoneNumber, string otp)
    {
        var result = await _smsService.SendSimpleAsync(
            to: phoneNumber,
            body: $"Your OTP code is: {otp}. Valid for 5 minutes."
        );

        if (result.IsSuccess)
        {
            Console.WriteLine($"SMS sent: {result.MessageId}");
        }
    }

    // Advanced SMS with metadata
    public async Task SendOrderNotificationAsync(string phoneNumber, string orderId)
    {
        var message = new SmsMessage
        {
            To = phoneNumber,
            Body = $"Your order #{orderId} has been shipped!",
            Type = SmsType.Transactional
        };

        message.AddMetadata("orderId", orderId);

        var result = await _smsService.SendAsync(message);
    }
}

Vonage Configuration:

{
  "Notification": {
    "SmsProvider": "Vonage",
    "EnableSms": true
  },
  "Vonage": {
    "ApiKey": "your-api-key",
    "ApiSecret": "your-api-secret",
    "FromPhoneNumber": "YourCompany"
  }
}

10.3. Push Notification

Supported Providers:

  • Firebase Cloud Messaging (FCM) - Google's push notification service

Configuration:

{
  "Notification": {
    "PushProvider": "Firebase",
    "EnablePush": true
  },
  "Firebase": {
    "ProjectId": "your-firebase-project-id",
    "ServerKey": "your-server-key",
    "ServiceAccountKeyPath": "path/to/service-account.json",
    "DefaultChannelId": "default-channel"
  }
}

Setup:

// Program.cs
builder.Services.AddNotifications(builder.Configuration);

// Or register specific push provider
builder.Services.AddFirebasePush(builder.Configuration);

Usage:

using Marventa.Framework.Features.Notification.Abstractions;
using Marventa.Framework.Features.Notification.Models;

public class NotificationService
{
    private readonly IPushNotificationService _pushService;

    // Simple push notification
    public async Task SendSimplePushAsync(string deviceToken)
    {
        var result = await _pushService.SendSimpleAsync(
            deviceToken: deviceToken,
            title: "New Message",
            body: "You have a new message!"
        );
    }

    // Advanced push notification
    public async Task SendAdvancedPushAsync(List<string> deviceTokens)
    {
        var message = new PushNotificationMessage
        {
            Title = "Order Update",
            Body = "Your order has been delivered!",
            Icon = "ic_notification",
            ImageUrl = "https://example.com/notification-image.png",
            Sound = "default",
            Priority = PushPriority.High,
            ChannelId = "orders",
            ClickAction = "OPEN_ORDER_ACTIVITY"
        };

        foreach (var token in deviceTokens)
        {
            message.AddDeviceToken(token);
        }

        message
            .AddData("orderId", "12345")
            .AddData("status", "delivered");

        var result = await _pushService.SendAsync(message);
    }

    // Send to topic/channel
    public async Task SendToTopicAsync()
    {
        var result = await _pushService.SendToTopicAsync(
            topic: "news",
            title: "Breaking News",
            body: "Important announcement!"
        );
    }
}

Platform-Specific Features:

  • Android: Channel ID, Color, Icon, Tag
  • iOS: Badge count, Sound
  • Both: Custom data payload, Click actions

11. Features - Localization (i18n)

Multi-language JSON resource support with culture switching:

// appsettings.json
{
  "Localization": {
    "DefaultCulture": "en-US",
    "SupportedCultures": ["en-US", "tr-TR", "de-DE"],
    "ResourcesPath": "Resources/Localization"
  }
}

// Resources/Localization/en-US.json
{
  "Welcome": "Welcome",
  "ProductNotFound": "Product not found"
}

// Resources/Localization/tr-TR.json
{
  "Welcome": "Hoş Geldiniz",
  "ProductNotFound": "Ürün bulunamadı"
}

// Usage
public class ProductService
{
    private readonly ILocalizationService _localization;

    public string GetWelcomeMessage()
    {
        return _localization.GetString("Welcome");
    }

    public void SwitchLanguage(string culture)
    {
        _localization.SetCurrentCulture(culture);
    }
}

Culture Detection:

  • Query string: ?culture=tr-TR
  • Cookie: .AspNetCore.Culture
  • Accept-Language header

12. Features - Template Engine

Dynamic content rendering with Liquid or Simple templates:

// appsettings.json
{
  "Templating": {
    "EngineType": "Simple", // or "Liquid"
    "TemplatesPath": "Templates"
  }
}

// Templates/order-confirmation.liquid
Hello {{ CustomerName }},
Your order #{{ OrderId }} totaling {{ Total | currency }} has been confirmed.

// Usage
public class EmailService
{
    private readonly ITemplateEngine _templateEngine;

    public async Task<string> RenderOrderEmail(Order order)
    {
        return await _templateEngine.RenderTemplateAsync("order-confirmation", new
        {
            CustomerName = order.CustomerName,
            OrderId = order.Id,
            Total = order.TotalAmount
        });
    }
}

13. Features - Reporting

13.1. PDF Report Generation

public class ReportService
{
    private readonly IReportGenerator _pdfGenerator;

    public async Task<byte[]> GeneratePdfReport(List<Product> products)
    {
        return await _pdfGenerator.GenerateAsync(products, new ReportOptions
        {
            Title = "Product Report",
            Orientation = PageOrientation.Landscape
        });
    }
}

13.2. Excel Report Generation

public async Task<byte[]> GenerateExcelReport(List<Order> orders)
{
    return await _excelGenerator.GenerateAsync(orders, new ReportOptions
    {
        SheetName = "Orders",
        AutoFitColumns = true
    });
}

13.3. CSV Report Generation

public async Task<byte[]> GenerateCsvReport(List<Customer> customers)
{
    return await _csvGenerator.GenerateAsync(customers);
}

14. Features - Payment Gateway

Stripe payment abstraction:

// appsettings.json
{
  "Stripe": {
    "SecretKey": "sk_test_...",
    "PublishableKey": "pk_test_...",
    "WebhookSecret": "whsec_..."
  }
}

// Usage
public class CheckoutService
{
    private readonly IPaymentGateway _paymentGateway;

    public async Task<PaymentResponse> ProcessPayment(decimal amount, string currency)
    {
        var result = await _paymentGateway.CreatePaymentAsync(new PaymentRequest
        {
            Amount = amount,
            Currency = currency,
            OrderId = "ORD-12345",
            CustomerEmail = "customer@example.com"
        });

        if (result.IsSuccess)
        {
            // Payment succeeded
            var paymentId = result.PaymentId;
        }

        return result;
    }
}

15. Features - Job Scheduler

Hangfire wrapper with cron support:

public class BackgroundJobService
{
    private readonly IJobScheduler _scheduler;

    // Enqueue immediate job
    public void SendWelcomeEmail(string userId)
    {
        _scheduler.Enqueue(() => SendEmailAsync(userId));
    }

    // Schedule delayed job
    public void SendReminderLater(string userId)
    {
        _scheduler.Schedule(() => SendReminderAsync(userId), TimeSpan.FromHours(24));
    }

    // Recurring job with cron
    public void SetupDailyReport()
    {
        _scheduler.AddOrUpdateRecurringJob(
            "daily-report",
            () => GenerateDailyReportAsync(),
            CronExpressions.Daily // Every day at midnight
        );
    }
}

// Built-in cron expressions
// - CronExpressions.EveryMinute
// - CronExpressions.Hourly
// - CronExpressions.Daily
// - CronExpressions.Weekly
// - CronExpressions.Monthly

16. Features - Audit Logging

Entity change tracking for compliance:

public class ProductService
{
    private readonly IAuditLogger _auditLogger;
    private readonly IRepository<Product> _productRepository;

    public async Task UpdateProduct(Guid id, UpdateProductDto dto)
    {
        var product = await _productRepository.GetByIdAsync(id);
        var oldProduct = product.Clone(); // Keep copy for audit

        product.UpdatePrice(dto.NewPrice);
        product.UpdateStock(dto.NewStock);

        // Audit log - automatically captures:
        // - Changed properties (Price, Stock)
        // - Old values vs New values
        // - User who made change (from JWT claims)
        // - IP address, UserAgent
        // - Timestamp
        await _auditLogger.LogUpdateAsync(oldProduct, product);

        await _productRepository.UpdateAsync(product);
    }

    public async Task<List<AuditLog>> GetProductHistory(Guid productId)
    {
        return await _auditLogger.GetAuditLogsAsync("Product", productId.ToString());
    }
}

Audit Log Model:

public class AuditLog
{
    public Guid Id { get; set; }
    public string? UserId { get; set; }
    public string? Username { get; set; }
    public string Action { get; set; } // Create, Update, Delete
    public string EntityType { get; set; }
    public string? EntityId { get; set; }
    public string? OldValues { get; set; } // JSON
    public string? NewValues { get; set; } // JSON
    public List<string> ChangedProperties { get; set; }
    public string? IpAddress { get; set; }
    public string? UserAgent { get; set; }
    public DateTime Timestamp { get; set; }
}

17. Features - Logging

17.1. Serilog

Configuration:

{
  "ApplicationName": "MyApp",
  "Serilog": {
    "MinimumLevel": "Information",
    "WriteTo": [
      { "Name": "Console" },
      { "Name": "File", "Args": { "path": "logs/log-.txt", "rollingInterval": "Day" } }
    ]
  }
}

Usage:

_logger.LogInformation("Product {ProductId} created", productId);
_logger.LogError(ex, "Failed to create product");

17.2. OpenTelemetry Tracing

Configuration:

{
  "OpenTelemetry": {
    "ServiceName": "MyApp",
    "OtlpEndpoint": "http://localhost:4317"
  }
}

Purpose: Automatically traces HTTP, Database, and External API calls.


18. Security - Authentication

18.1. JWT Authentication Service

Configuration:

{
  "Jwt": {
    "Secret": "your-super-secret-key-at-least-32-characters",
    "Issuer": "MyApp",
    "Audience": "MyApp",
    "ExpirationMinutes": 60
  }
}

Generate Access Token:

using Marventa.Framework.Security.Authentication.Abstractions;

public class AuthService
{
    private readonly IJwtService _jwtService;

    // Simple usage
    public string Login(User user)
    {
        var token = _jwtService.GenerateAccessToken(
            userId: user.Id.ToString(),
            email: user.Email,
            roles: new[] { "Admin" },
            additionalClaims: new Dictionary<string, string>
            {
                ["department"] = "IT",
                ["permission"] = "products.write"
            }
        );

        return token;
    }
}

Validate and Extract Claims:

// Validate token
var principal = _jwtService.ValidateAccessToken(token);
if (principal == null)
{
    // Token invalid or expired
}

// Get user ID from token
var userId = _jwtService.GetUserIdFromToken(token);

// Get all claims
var claims = _jwtService.GetClaimsFromToken(token);

// Check if token expired
var isExpired = _jwtService.IsTokenExpired(token);

// Get remaining lifetime
var remainingTime = _jwtService.GetTokenRemainingLifetime(token);

12.2. Refresh Token Generation

Purpose: Generate cryptographically secure refresh token strings.

Important: The framework only provides token generation. You must implement your own:

  • RefreshToken entity in your domain model
  • Repository for database storage
  • Validation, rotation, and revocation logic

Generate Refresh Token:

using Marventa.Framework.Security.Authentication.Abstractions;

public class AuthService
{
    private readonly IJwtService _jwtService;
    private readonly IRefreshTokenRepository _refreshTokenRepository;
    private readonly IUnitOfWork _unitOfWork;

    public async Task<LoginResponse> LoginAsync(string email, string password)
    {
        // Validate user credentials...

        // Generate tokens
        var accessToken = _jwtService.GenerateAccessToken(user.Id.ToString(), user.Email);
        var refreshTokenString = _jwtService.GenerateRefreshToken();

        // Create your own domain entity
        var refreshToken = new Domain.Entities.RefreshToken
        {
            Token = refreshTokenString,
            UserId = user.Id,
            ExpiresAt = DateTime.UtcNow.AddDays(7),
            CreatedByIp = HttpContext.Connection.RemoteIpAddress?.ToString()
        };

        // Save to your database
        await _refreshTokenRepository.AddAsync(refreshToken);
        await _unitOfWork.SaveChangesAsync();

        return new LoginResponse
        {
            AccessToken = accessToken,
            RefreshToken = refreshTokenString,
            ExpiresAt = refreshToken.ExpiresAt
        };
    }

    public async Task<LoginResponse> RefreshTokenAsync(string refreshToken)
    {
        // Validate from your database
        var token = await _refreshTokenRepository.GetByTokenAsync(refreshToken);
        if (token == null || token.IsExpired || token.IsRevoked)
        {
            throw new UnauthorizedException("Invalid refresh token");
        }

        // Optional: Implement token rotation
        token.RevokedAt = DateTime.UtcNow;
        await _refreshTokenRepository.UpdateAsync(token);

        // Generate new tokens
        var accessToken = _jwtService.GenerateAccessToken(token.UserId.ToString(), user.Email);
        var newRefreshTokenString = _jwtService.GenerateRefreshToken();

        var newRefreshToken = new Domain.Entities.RefreshToken
        {
            Token = newRefreshTokenString,
            UserId = token.UserId,
            ExpiresAt = DateTime.UtcNow.AddDays(7),
            ReplacedByToken = token.Token
        };

        await _refreshTokenRepository.AddAsync(newRefreshToken);
        await _unitOfWork.SaveChangesAsync();

        return new LoginResponse
        {
            AccessToken = accessToken,
            RefreshToken = newRefreshTokenString,
            ExpiresAt = newRefreshToken.ExpiresAt
        };
    }
}

Your Domain Entity Example:

public class RefreshToken : Entity<Guid>
{
    public string Token { get; set; }
    public Guid UserId { get; set; }
    public DateTime ExpiresAt { get; set; }
    public DateTime? RevokedAt { get; set; }
    public string? CreatedByIp { get; set; }
    public string? ReplacedByToken { get; set; }

    public bool IsExpired => DateTime.UtcNow >= ExpiresAt;
    public bool IsRevoked => RevokedAt != null;
    public bool IsActive => !IsExpired && !IsRevoked;
}

12.3. Password Service

Purpose: Secure password hashing with Argon2id (winner of Password Hashing Competition 2015) and strength validation.

Why Argon2id?

  • ✅ 2-6x FASTER than BCrypt
  • ✅ More secure against GPU/ASIC attacks (high memory requirement)
  • ✅ OWASP recommended (2024)
  • ✅ Automatic migration from BCrypt hashes
using Marventa.Framework.Security.Encryption.Abstractions;

public class UserService
{
    private readonly IPasswordService _passwordService;

    // Hash password with Argon2id
    public async Task RegisterAsync(string email, string password)
    {
        // Validate password strength
        var (isValid, errorMessage) = _passwordService.ValidatePasswordStrength(
            password: password,
            minLength: 8,
            requireUppercase: true,
            requireLowercase: true,
            requireDigit: true,
            requireSpecialChar: true
        );

        if (!isValid)
        {
            throw new BusinessException($"Weak password: {errorMessage}");
        }

        // Hash with Argon2id (OWASP settings: m=19456, t=2, p=1)
        var hashedPassword = _passwordService.HashPassword(password);

        // Save user with hashed password...
    }

    // Verify password (supports both Argon2id and legacy BCrypt)
    public async Task<bool> LoginAsync(string email, string password)
    {
        var user = await _userRepository.GetByEmailAsync(email);
        if (user == null)
            return false;

        var isValid = _passwordService.VerifyPassword(password, user.PasswordHash);

        // Automatic migration from BCrypt to Argon2id
        if (isValid && _passwordService.NeedsRehash(user.PasswordHash))
        {
            user.PasswordHash = _passwordService.HashPassword(password);
            await _userRepository.UpdateAsync(user);
        }

        return isValid;
    }

    // Generate secure random password
    public string GenerateTemporaryPassword()
    {
        return _passwordService.GenerateSecurePassword(
            length: 16,
            includeSpecialCharacters: true
        );
    }
}

Hash Format:

Argon2id: $argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash>
BCrypt (legacy): $2a$12$<salt+hash>

Performance Comparison: | Algorithm | Hash Time | Security | |-----------|-----------|----------| | Argon2id (current) | 30-50ms | ✅ GPU/ASIC resistant | | BCrypt (legacy) | 100-300ms | ⚠️ Vulnerable to GPU attacks |

Migration is automatic! Existing BCrypt hashes are verified normally, then upgraded to Argon2id on next successful login.

12.4. AES Encryption

Purpose: Symmetric encryption for sensitive data.

using Marventa.Framework.Security.Encryption;

var encryption = new AesEncryption(
    key: "your-32-character-secret-key!",
    iv: "your-16-char-iv"
);

// Encrypt sensitive data
var encrypted = encryption.Encrypt("sensitive data");

// Decrypt
var decrypted = encryption.Decrypt(encrypted);

Use Cases:

  • Encrypting database connection strings
  • Storing sensitive configuration values
  • Protecting PII (Personal Identifiable Information)

19. Security - Authorization

19.1. Permission Based Authorization

[Authorize]
[RequirePermission("products.write")]
public async Task<IActionResult> Create([FromBody] CreateProductCommand command)
{
    var result = await _mediator.Send(command);
    return Ok(result);
}

20. Security - Rate Limiting

Configuration:

{
  "RateLimiting": {
    "Strategy": "IpAddress",
    "RequestLimit": 100,
    "TimeWindowSeconds": 60
  }
}

Purpose: Automatically limits to 100 requests per 60 seconds per IP.

Response Headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1633024800

21. Infrastructure - Multi-Tenancy

Configuration:

{
  "MultiTenancy": {
    "Strategy": "Header",
    "HeaderName": "X-Tenant-Id"
  }
}

Usage:

var tenantId = _tenantContext.TenantId;
var tenantName = _tenantContext.TenantName;

var data = _repository.GetAll()
    .Where(x => x.TenantId == tenantId)
    .ToList();

Client Request:

curl -H "X-Tenant-Id: tenant-123" https://api.myapp.com/products

22. Infrastructure - Health Checks

Configuration:

{
  "HealthChecks": {
    "Enabled": "true"
  }
}

Purpose: Creates /health endpoint, automatically monitors Database/Redis/RabbitMQ.

Check:

curl http://localhost:5000/health

23. Infrastructure - API Versioning

Purpose: Provides flexible API versioning strategies.

Configuration:

{
  "ApiVersioning": {
    "Enabled": true,
    "DefaultVersion": "1.0",
    "ReportApiVersions": true,
    "AssumeDefaultVersionWhenUnspecified": true,
    "VersioningType": "UrlSegment",
    "HeaderName": "X-API-Version",
    "QueryStringParameterName": "api-version"
  }
}

Versioning Types:

  • UrlSegment - /api/v1/products (default)
  • QueryString - /api/products?api-version=1.0
  • Header - Header: X-API-Version: 1.0
  • MediaType - Accept: application/json;v=1.0

Usage in Controllers:

[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
public class ProductsV1Controller : ControllerBase
{
    [HttpGet]
    public IActionResult GetProducts()
    {
        return Ok(new[] { "Product 1", "Product 2" });
    }
}

[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
public class ProductsV2Controller : ControllerBase
{
    [HttpGet]
    public IActionResult GetProducts()
    {
        return Ok(new { products = new[] { "Product 1", "Product 2" }, version = "2.0" });
    }
}

Response Headers:

api-supported-versions: 1.0, 2.0
api-deprecated-versions: (none)

24. Infrastructure - Swagger/OpenAPI

Purpose: Auto-configured OpenAPI documentation with JWT support and environment restrictions.

Configuration:

{
  "Swagger": {
    "Enabled": true,
    "Title": "My API",
    "Description": "My API Documentation",
    "Version": "v1",
    "RequireAuthorization": true,
    "EnvironmentRestriction": ["Development", "Staging"],
    "Contact": {
      "Name": "API Support",
      "Email": "support@example.com",
      "Url": "https://example.com/support"
    },
    "License": {
      "Name": "MIT",
      "Url": "https://opensource.org/licenses/MIT"
    }
  }
}

Features:

  • ✅ Automatic JWT Bearer integration
  • ✅ Multi-version support (when API Versioning enabled)
  • ✅ XML comments auto-included
  • ✅ Environment-based restrictions
  • ✅ Swagger UI auto-configured

Access:

# Development/Staging only (based on EnvironmentRestriction)
https://localhost:5001/swagger

Usage in Program.cs:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMarventa(builder.Configuration);

var app = builder.Build();

// Pass IWebHostEnvironment for environment-based Swagger
app.UseMarventa(builder.Configuration, app.Environment);

app.Run();

Controller XML Comments:

/// <summary>
/// Creates a new product
/// </summary>
/// <param name="command">Product creation data</param>
/// <returns>The created product ID</returns>
/// <response code="200">Product created successfully</response>
/// <response code="400">Invalid request</response>
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Create([FromBody] CreateProductCommand command)
{
    var result = await _mediator.Send(command);
    return result.IsSuccess ? Ok(result.Value) : BadRequest(result.ErrorMessage);
}

25. Middleware - Exception Handling

Purpose: Catches all exceptions and returns standard format. Auto-active!

Custom Exceptions:

throw new NotFoundException("Product not found");
throw new BusinessException("Insufficient stock");
throw new UnauthorizedException("Invalid credentials");

26. Enterprise Patterns

26.1. Transactional Outbox Pattern

The Outbox Pattern ensures reliable event publishing by storing events in the database in the same transaction as business data.

Setup:

// Program.cs
builder.Services.AddDbContext<YourDbContext>();
builder.Services.AddScoped<DbContext>(sp => sp.GetRequiredService<YourDbContext>());
builder.Services.AddOutbox(); // Registers outbox services and background processor

Usage:

// Domain Entity
public class Product : Entity<Guid>, IHasDomainEvents
{
    public void Create()
    {
        AddDomainEvent(new ProductCreatedEvent(Id, Name, Price));
    }
}

// Event Handler
public class ProductCreatedEventHandler : INotificationHandler<ProductCreatedEvent>
{
    public async Task Handle(ProductCreatedEvent notification, CancellationToken cancellationToken)
    {
        // Event will be published reliably via outbox
        await _eventBus.PublishAsync(notification);
    }
}

How it works:

  1. Domain events saved to OutboxMessages table in same transaction
  2. Background OutboxProcessor polls every 30 seconds
  3. Processes pending messages and publishes events
  4. Marks messages as processed
  5. Cleans up old messages (7-day retention)

26.2. Repository Specification Pattern

Build reusable, composable query specifications for complex queries.

Create Specification:

public class ActiveProductsSpecification : BaseSpecification<Product>
{
    public ActiveProductsSpecification()
    {
        // Filtering
        Criteria = p => p.IsActive && !p.IsDeleted;

        // Eager Loading
        AddInclude(p => p.Category);
        AddInclude(p => p.Supplier);

        // Ordering
        AddOrderBy(p => p.Name);

        // Pagination
        ApplyPaging(0, 20);
    }
}

public class ProductsByPriceRangeSpec : BaseSpecification<Product>
{
    public ProductsByPriceRangeSpec(decimal minPrice, decimal maxPrice)
    {
        Criteria = p => p.Price >= minPrice && p.Price <= maxPrice;
        AddOrderByDescending(p => p.Price);
    }
}

Use Specification:

// In Repository or Command Handler
var spec = new ActiveProductsSpecification();
var products = await _repository.FindAsync(spec);

var priceSpec = new ProductsByPriceRangeSpec(50m, 200m);
var filteredProducts = await _repository.FindAsync(priceSpec);

Available Methods:

  • Criteria - WHERE clause filter
  • AddInclude() / AddInclude(string) - Eager loading
  • AddOrderBy() / AddOrderByDescending() - Sorting
  • ApplyPaging(skip, take) - Pagination

26.3. Idempotency Pattern

Prevent duplicate request processing with distributed caching.

Setup:

// Program.cs
builder.Services.AddDistributedMemoryCache(); // or AddStackExchangeRedisCache
builder.Services.AddIdempotency();

var app = builder.Build();
app.UseRouting();
app.UseIdempotency(); // Must be after UseRouting()

Usage:

// Client sends Idempotency-Key header
POST /api/orders
Headers:
  Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

Body: { "productId": "123", "quantity": 2 }

How it works:

  1. Middleware detects Idempotency-Key header
  2. Checks cache for existing response
  3. If found, returns cached response (duplicate detected)
  4. If not found, processes request and caches response
  5. Default 24-hour expiration
  6. Only caches successful responses (2xx status codes)

26.4. Circuit Breaker & Resilience

Add resilience to HTTP clients with Polly integration.

Setup:

// Program.cs
builder.Services.AddResilientHttpClient("PaymentService", "https://api.payment.com")
    .AddHttpMessageHandler(() => new AuthHeaderHandler());

// Or configure manually
builder.Services.AddHttpClient("OrderService")
    .AddPolicyHandler(ResilienceExtensions.GetRetryPolicy(3))
    .AddPolicyHandler(ResilienceExtensions.GetCircuitBreakerPolicy(5, TimeSpan.FromSeconds(30)));

Usage:

public class PaymentService
{
    private readonly IHttpClientFactory _httpClientFactory;

    public async Task<bool> ProcessPayment(PaymentRequest request)
    {
        var client = _httpClientFactory.CreateClient("PaymentService");
        var response = await client.PostAsJsonAsync("/payments", request);
        return response.IsSuccessStatusCode;
    }
}

Policies:

  • Retry Policy: 3 retries with exponential backoff (2s, 4s, 8s)
  • Circuit Breaker: Opens after 5 consecutive failures, stays open for 30s
  • Timeout: Configurable per request

27. Advanced Security

27.1. API Key Authentication

Header-based authentication with role support.

Setup:

// Program.cs
builder.Services.AddApiKeyAuthentication();

// appsettings.json
{
  "Authentication": {
    "ApiKeys": [
      {
        "Key": "your-api-key-12345",
        "Owner": "MobileApp",
        "Roles": "User,Customer"
      },
      {
        "Key": "admin-key-67890",
        "Owner": "BackofficeAdmin",
        "Roles": "Admin,User"
      }
    ]
  }
}

Usage:

[Authorize(AuthenticationSchemes = "ApiKey")]
[ApiController]
public class ProductsController : ControllerBase
{
    [HttpGet]
    [Authorize(Roles = "User")] // Requires User role
    public async Task<IActionResult> GetProducts() { }

    [HttpDelete("{id}")]
    [Authorize(Roles = "Admin")] // Requires Admin role
    public async Task<IActionResult> DeleteProduct(Guid id) { }
}

// Client Request
GET /api/products
Headers:
  X-API-Key: your-api-key-12345

Features:

  • Header name: X-API-Key
  • Claims-based authentication
  • Role-based authorization
  • API key masking in logs (shows first 4 chars)

27.2. Request/Response Logging

Comprehensive logging with automatic sensitive data masking.

Setup:

// Program.cs
builder.Services.Configure<LoggingOptions>(builder.Configuration.GetSection("LoggingOptions"));

var app = builder.Build();
app.UseMiddleware<RequestResponseLoggingMiddleware>(); // Must be early in pipeline

// appsettings.json
{
  "LoggingOptions": {
    "EnableRequestResponseLogging": true,
    "MaxBodyLogSize": 4096,
    "LogRequestHeaders": true,
    "LogRequestBody": true,
    "LogResponseHeaders": true,
    "LogResponseBody": true,
    "SensitiveHeaders": ["Authorization", "X-API-Key", "Cookie"],
    "SensitiveBodyFields": ["password", "token", "secret", "apikey"]
  }
}

Features:

  • Automatic header masking (Authorization, X-API-Key, Cookie)
  • Automatic body field masking (password, token, secret, etc.)
  • Configurable max body size
  • JSON field detection and masking
  • Response time tracking
  • Content-type aware (only logs text-based content)

Example Log Output:

[INFO] HTTP GET /api/users/login
Request Headers: {"User-Agent": "PostmanRuntime/7.32.0", "Authorization": "***MASKED***"}
Request Body: {"username":"john","password":"***MASKED***","apiKey":"***MASKED***"}
Response Status: 200 OK
Response Body: {"success":true,"token":"***MASKED***","user":{...}}
Response Time: 45ms

28. Configuration - appsettings.json

Complete configuration example:

{
  "ApplicationName": "MyApp",

  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=MyDb;Trusted_Connection=true;"
  },

  "Cors": {
    "AllowedOrigins": ["http://localhost:3000", "https://myapp.com"]
  },

  "ApiVersioning": {
    "Enabled": true,
    "DefaultVersion": "1.0",
    "ReportApiVersions": true,
    "AssumeDefaultVersionWhenUnspecified": true,
    "VersioningType": "UrlSegment"
  },

  "Swagger": {
    "Enabled": true,
    "Title": "My API",
    "Description": "My API Documentation",
    "Version": "v1",
    "RequireAuthorization": true,
    "EnvironmentRestriction": ["Development", "Staging"]
  },

  "Jwt": {
    "Secret": "your-super-secret-key-at-least-32-characters-long",
    "Issuer": "MyApp",
    "Audience": "MyApp",
    "ExpirationMinutes": 60
  },

  "Caching": {
    "Type": "Hybrid"
  },

  "MemoryCache": {
    "SizeLimit": 1024,
    "CompactionPercentage": 0.25,
    "ExpirationScanFrequency": "00:01:00"
  },

  "OutputCache": {
    "Enabled": true,
    "DefaultExpirationSeconds": 60,
    "VaryByQuery": true,
    "VaryByHeader": false,
    "VaryByHeaderNames": []
  },

  "Redis": {
    "ConnectionString": "localhost:6379",
    "InstanceName": "MyApp:"
  },

  "MultiTenancy": {
    "Strategy": "Header",
    "HeaderName": "X-Tenant-Id"
  },

  "RateLimiting": {
    "Strategy": "IpAddress",
    "RequestLimit": 100,
    "TimeWindowSeconds": 60
  },

  "RabbitMQ": {
    "Host": "localhost",
    "VirtualHost": "/",
    "Username": "guest",
    "Password": "guest"
  },

  "Kafka": {
    "BootstrapServers": "localhost:9092",
    "GroupId": "myapp-group"
  },

  "MassTransit": {
    "Enabled": "true"
  },

  "LocalStorage": {
    "BasePath": "D:/uploads",
    "BaseUrl": "https://myapp.com/files"
  },

  "Azure": {
    "Storage": {
      "ConnectionString": "DefaultEndpointsProtocol=https;AccountName=...",
      "ContainerName": "uploads"
    }
  },

  "AWS": {
    "AccessKey": "your-access-key",
    "SecretKey": "your-secret-key",
    "Region": "us-east-1",
    "BucketName": "my-bucket"
  },

  "MongoDB": {
    "ConnectionString": "mongodb://localhost:27017",
    "DatabaseName": "MyDatabase"
  },

  "Elasticsearch": {
    "Uri": "http://localhost:9200"
  },

  "HealthChecks": {
    "Enabled": "true"
  },

  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "System": "Warning"
      }
    },
    "WriteTo": [
      { "Name": "Console" },
      {
        "Name": "File",
        "Args": {
          "path": "logs/log-.txt",
          "rollingInterval": "Day",
          "retainedFileCountLimit": 7
        }
      }
    ]
  },

  "OpenTelemetry": {
    "ServiceName": "MyApp",
    "OtlpEndpoint": "http://localhost:4317"
  }
}

🎉 That's It!

Your application now has:

Core: Domain Driven Design (Entity, Aggregate, ValueObject, DomainEvent) ✅ Behaviors: CQRS (MediatR + FluentValidation + Mapster + Logging + Performance) ✅ Infrastructure: Repository Pattern, Unit of Work, Multi-Tenancy, Health Checks, Data Seeding ✅ API: Swagger/OpenAPI, API Versioning (URL/Query/Header), XML Documentation ✅ Features: Caching, Event Bus (RabbitMQ/Kafka/MassTransit), Storage, Search, Logging ✅ Security: JWT Auth, CORS, Permission Authorization, Rate Limiting, Password Hashing ✅ Middleware: Global Exception Handling with correct pipeline order

With just 2 lines of setup! 🚀

🆕 What's New in v5.2.0 - MongoDB Repository Pattern & .NET 10 Support

📦 MongoDB Repository Pattern:

  • Full MongoDB Integration: Complete repository pattern implementation for MongoDB
  • Document-based Entities: Support for MongoDB BSON ObjectId and document attributes
  • Generic Repository: IMongoRepository<TEntity> with async CRUD operations
  • Specification Pattern: MongoDB-specific specification support
  • Unit of Work: Transaction support for MongoDB sessions
  • Flexible Configuration: Easy setup with connection string and database name

🚀 .NET 10 Support:

  • Framework Upgrade: Full compatibility with .NET 10.0
  • Performance Improvements: Leveraging latest .NET 10 runtime optimizations
  • C# 13 Features: Support for latest C# language features
  • Modern APIs: Updated to use latest ASP.NET Core 10 APIs

📚 Enhanced Documentation:

  • Added MongoDB setup guide
  • MongoDB repository examples
  • Configuration samples
  • Migration guide from SQL to MongoDB

🆕 What's New in v5.1.0 - Complete Enterprise Toolkit

🎉 Notification Services:

  • Email Services: SMTP, SendGrid, AWS SES support with attachments and HTML
  • SMS Services: Twilio and Vonage integration for global messaging
  • Push Notifications: Firebase Cloud Messaging for mobile apps
  • Unified Interface: Consistent API across all notification providers
  • Fluent Builders: Easy message construction with builder pattern

🌍 Localization (i18n):

  • Multi-Language Support: JSON-based resource files
  • Culture Detection: Query string, cookie, and header-based culture switching
  • ILocalizationService: Simple API for string translations

📄 Template Engine:

  • Liquid Templates: DotLiquid integration for advanced templating
  • Simple Templates: Lightweight string interpolation
  • Email Templates: Pre-built email templates

📊 Reporting:

  • PDF Reports: QuestPDF integration
  • Excel Reports: EPPlus for spreadsheet generation
  • CSV Reports: Fast CSV export

💳 Payment Gateway:

  • Stripe Integration: Payment processing with webhook support
  • Payment Models: Type-safe payment requests and responses

⏰ Job Scheduler:

  • Hangfire Integration: Background job processing
  • Cron Support: Schedule recurring jobs
  • Dashboard: Built-in monitoring UI

📝 Audit Logging:

  • Entity Change Tracking: Automatic audit trail
  • User Context: Track who made changes
  • IP & UserAgent Logging: Full request context

🆕 What's New in v5.0.0 - MAJOR SECURITY AND ARCHITECTURE UPDATE

🔒 Critical Security Fixes:

  • AES-GCM Encryption with Random Nonce: Fixed encryption vulnerability by using AES-GCM with cryptographically random nonce for each operation (previously used static IV with CBC mode)
  • Entity Equality Implementation: Corrected GetHashCode() for transient entities to prevent hash collisions
  • JWT Secret Validation: Added minimum 32-character validation for JWT secrets
  • Domain Events Transaction Safety: Events now dispatched AFTER SaveChanges for transaction consistency

🏗️ Repository & Database Enhancements:

  • Pagination Support: Added GetPagedAsync() with eager loading support to prevent memory issues
  • N+1 Query Prevention: Include/ThenInclude support in all query methods
  • Soft Delete Global Filters: Automatic filtering of soft-deleted entities via ISoftDeletable
  • ICurrentUserService: Claims-based current user service for audit trails

📦 New Enterprise Patterns:

  • Transactional Outbox Pattern: Ensures reliable event publishing with background processor
  • Repository Specification Pattern: Reusable, composable query specifications with filtering, ordering, and pagination
  • Idempotency Pattern: Prevents duplicate request processing with distributed caching
  • Circuit Breaker with Polly: Resilient HTTP client with retry and circuit breaker policies

🔐 Advanced Security Features:

  • API Key Authentication: Header-based authentication with role support
  • Request/Response Logging: Comprehensive logging with automatic sensitive data masking (passwords, tokens, API keys)
  • Configuration Validation: Startup validation for all configuration options

🚀 Infrastructure Improvements:

  • Redis Prefix Removal: Efficient RemoveByPrefixAsync() implementation
  • Performance Middleware: Automatic response time tracking with slow request logging
  • Code Quality: Eliminated all magic strings, improved error handling, enhanced XML documentation

🆕 What's New in v4.6.0

  • Password Hashing Migration: Migrated from BCrypt to Argon2id for enhanced security
    • More secure against GPU/ASIC attacks (high memory requirement)
    • OWASP recommended settings (m=19456, t=2, p=1)
    • Automatic backward compatibility with BCrypt hashes
    • Seamless migration on user login

📄 License

MIT License - See LICENSE for details.


📧 Support

For questions, please open an issue on GitHub Issues.

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
5.2.0 229 10/13/2025 5.2.0 is deprecated because it is no longer maintained.
5.1.0 277 10/5/2025 5.1.0 is deprecated because it is no longer maintained.
5.0.0 184 10/4/2025 5.0.0 is deprecated because it is no longer maintained.
4.6.0 196 10/3/2025 4.6.0 is deprecated because it is no longer maintained.
4.5.5 215 10/2/2025 4.5.5 is deprecated because it is no longer maintained.
4.5.4 210 10/2/2025 4.5.4 is deprecated because it is no longer maintained.
4.5.3 208 10/2/2025 4.5.3 is deprecated because it is no longer maintained.
4.5.2 209 10/2/2025 4.5.2 is deprecated because it is no longer maintained.
4.5.1 211 10/2/2025 4.5.1 is deprecated because it is no longer maintained.
4.5.0 212 10/2/2025 4.5.0 is deprecated because it is no longer maintained.
4.4.0 218 10/1/2025 4.4.0 is deprecated because it is no longer maintained.
4.3.0 216 10/1/2025 4.3.0 is deprecated because it is no longer maintained.
4.2.0 218 10/1/2025 4.2.0 is deprecated because it is no longer maintained.
4.1.0 210 10/1/2025 4.1.0 is deprecated because it is no longer maintained.
4.0.2 218 10/1/2025 4.0.2 is deprecated because it is no longer maintained.
4.0.1 209 10/1/2025 4.0.1 is deprecated because it is no longer maintained.
4.0.0 286 9/30/2025 4.0.0 is deprecated because it is no longer maintained.
3.5.2 219 9/30/2025 3.5.2 is deprecated because it is no longer maintained.
3.5.1 250 9/30/2025 3.5.1 is deprecated because it is no longer maintained.
3.4.1 254 9/30/2025 3.4.1 is deprecated because it is no longer maintained.
3.4.0 249 9/30/2025 3.4.0 is deprecated because it is no longer maintained.
3.3.2 261 9/30/2025 3.3.2 is deprecated because it is no longer maintained.
3.2.0 253 9/30/2025 3.2.0 is deprecated because it is no longer maintained.
3.1.0 252 9/29/2025 3.1.0 is deprecated because it is no longer maintained.
3.0.1 251 9/29/2025 3.0.1 is deprecated because it is no longer maintained.
3.0.1-preview-20250929165802 245 9/29/2025 3.0.1-preview-20250929165802 is deprecated because it is no longer maintained.
3.0.0 248 9/29/2025 3.0.0 is deprecated because it is no longer maintained.
3.0.0-preview-20250929164242 251 9/29/2025 3.0.0-preview-20250929164242 is deprecated because it is no longer maintained.
3.0.0-preview-20250929162455 248 9/29/2025 3.0.0-preview-20250929162455 is deprecated because it is no longer maintained.
2.12.0-preview-20250929161039 242 9/29/2025 2.12.0-preview-20250929161039 is deprecated because it is no longer maintained.
2.11.0 253 9/29/2025 2.11.0 is deprecated because it is no longer maintained.
2.10.0 253 9/29/2025 2.10.0 is deprecated because it is no longer maintained.
2.9.0 247 9/29/2025 2.9.0 is deprecated because it is no longer maintained.
2.8.0 249 9/29/2025 2.8.0 is deprecated because it is no longer maintained.
2.7.0 260 9/29/2025 2.7.0 is deprecated because it is no longer maintained.
2.6.0 254 9/28/2025 2.6.0 is deprecated because it is no longer maintained.
2.5.0 260 9/28/2025 2.5.0 is deprecated because it is no longer maintained.
2.4.0 252 9/28/2025 2.4.0 is deprecated because it is no longer maintained.
2.3.0 253 9/28/2025 2.3.0 is deprecated because it is no longer maintained.
2.2.0 255 9/28/2025 2.2.0 is deprecated because it is no longer maintained.
2.1.0 253 9/26/2025 2.1.0 is deprecated because it is no longer maintained.
2.0.9 257 9/26/2025 2.0.9 is deprecated because it is no longer maintained.
2.0.5 250 9/25/2025 2.0.5 is deprecated because it is no longer maintained.
2.0.4 256 9/25/2025 2.0.4 is deprecated because it is no longer maintained.
2.0.3 261 9/25/2025 2.0.3 is deprecated because it is no longer maintained.
2.0.1 257 9/25/2025 2.0.1 is deprecated because it is no longer maintained.
2.0.0 258 9/25/2025 2.0.0 is deprecated because it is no longer maintained.
1.1.2 334 9/24/2025 1.1.2 is deprecated because it is no longer maintained.
1.1.1 335 9/24/2025 1.1.1 is deprecated because it is no longer maintained.
1.1.0 253 9/24/2025 1.1.0 is deprecated because it is no longer maintained.
1.0.0 258 9/24/2025 1.0.0 is deprecated because it is no longer maintained.