Marventa.Framework 2.0.5

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

Marventa.Framework

A comprehensive .NET 9.0 enterprise e-commerce framework with multi-tenancy, JWT authentication, CQRS, messaging infrastructure, and complete e-commerce domain modules.

Features

🏗️ Core Architecture

  • Multi-Tenancy - Complete tenant isolation with policy-based authorization
  • CQRS & MediatR - Command Query Responsibility Segregation with pipeline behaviors
  • Clean Architecture - Domain-driven design with clear layer separation
  • Dependency Injection - Built-in DI container integration

🔐 Security & Authentication

  • JWT Authentication - Token-based authentication and authorization
  • Security & Encryption - Data protection, encryption, and secure storage
  • API Versioning - Header, query, and URL-based versioning strategies
  • Rate Limiting - Tenant-aware and endpoint-specific rate limiting

💾 Data & Persistence

  • Entity Framework Core - Repository pattern with tenant scoping
  • Database Seeding - Multi-tenant seed data management
  • Distributed Locking - Redis-based distributed locks
  • Caching - Redis distributed and in-memory caching with tenant isolation

📨 Messaging & Communication

  • RabbitMQ + MassTransit - Reliable message bus with retry policies
  • Kafka Integration - High-throughput event streaming
  • Outbox/Inbox Patterns - Guaranteed message delivery
  • Email & SMS Services - Template-based communication

💰 E-Commerce Domain

  • Payment Processing - Complete payment domain with events and state management
  • Shipping Management - End-to-end shipping lifecycle with tracking
  • Money/Currency - Multi-currency value objects with exchange rates
  • Order Management - Complete order lifecycle with domain events

🔄 Workflow & Orchestration

  • Saga Patterns - Long-running business process orchestration
  • Background Jobs - Hangfire-based job processing
  • HTTP Idempotency - Correlation tracking for safe retries

🔍 Monitoring & Observability

  • Structured Logging - Serilog with tenant and correlation context
  • OpenTelemetry - Distributed tracing and metrics
  • Health Checks - Database, cache, and service monitoring
  • Feature Flags - Dynamic feature toggles with tenant support

🛠️ Developer Experience

  • Validation - FluentValidation with RFC 7807 Problem Details
  • Circuit Breaker - HTTP resilience patterns with fallback
  • Search & Analytics - Elasticsearch full-text search
  • Cloud Storage - S3-compatible storage abstraction

Installation

dotnet add package Marventa.Framework

Quick Start

// Program.cs
using Marventa.Framework;

var builder = WebApplication.CreateBuilder(args);

// Add Marventa Framework services
builder.Services.AddMarventa();

var app = builder.Build();

// Use Marventa Framework middleware
app.UseMarventa();

app.Run();

Complete Feature Guide

🏢 Multi-Tenancy

Configuration
{
  "MultiTenancy": {
    "ResolutionStrategy": "Header", // Header, Subdomain, Claim
    "HeaderName": "X-Tenant-Id",
    "DefaultTenant": "default"
  }
}
Implementation
// Tenant Entity
public class Tenant : ITenant
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string ConnectionString { get; set; }
    public Dictionary<string, object> Properties { get; set; }
}

// Tenant Context Usage
public class ProductService
{
    private readonly ITenantContext _tenantContext;
    private readonly IRepository<Product> _repository;

    public async Task<Product> CreateProductAsync(CreateProductRequest request)
    {
        var product = new Product
        {
            Name = request.Name,
            TenantId = _tenantContext.TenantId // Automatically set
        };

        await _repository.AddAsync(product);
        return product;
    }
}

// Tenant Authorization
[TenantAuthorize("products:read")]
public class ProductController : ControllerBase
{
    [HttpGet]
    public async Task<IActionResult> GetProducts()
    {
        // Only products for current tenant will be returned
        return Ok(await _productService.GetProductsAsync());
    }
}

🔐 JWT Authentication

Configuration
{
  "JWT": {
    "SecretKey": "your-super-secret-key-at-least-32-characters-long",
    "Issuer": "your-app-name",
    "Audience": "your-app-users",
    "ExpiryInMinutes": 60,
    "RefreshTokenExpiryInDays": 7
  }
}
Implementation
// Token Service Usage
public class AuthController : ControllerBase
{
    private readonly ITokenService _tokenService;
    private readonly ICurrentUserService _currentUser;

    [HttpPost("login")]
    public async Task<IActionResult> Login(LoginRequest request)
    {
        // Validate user credentials here
        var user = await ValidateUserAsync(request);

        var claims = new[]
        {
            new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
            new Claim(ClaimTypes.Name, user.Username),
            new Claim(ClaimTypes.Email, user.Email),
            new Claim("tenant_id", user.TenantId)
        };

        var accessToken = await _tokenService.GenerateAccessTokenAsync(claims);
        var refreshToken = await _tokenService.GenerateRefreshTokenAsync();

        return Ok(new TokenInfo
        {
            AccessToken = accessToken,
            RefreshToken = refreshToken,
            ExpiresAt = DateTime.UtcNow.AddMinutes(60),
            TokenType = "Bearer"
        });
    }

    [HttpPost("refresh")]
    public async Task<IActionResult> RefreshToken(RefreshTokenRequest request)
    {
        var principal = await _tokenService.ValidateTokenAsync(request.RefreshToken);
        if (principal == null)
            return Unauthorized();

        var newToken = await _tokenService.GenerateAccessTokenAsync(principal.Claims);
        return Ok(new { Token = newToken });
    }

    [HttpPost("logout")]
    [Authorize]
    public async Task<IActionResult> Logout()
    {
        var token = HttpContext.Request.Headers.Authorization
            .FirstOrDefault()?.Split(" ").Last();

        if (!string.IsNullOrEmpty(token))
            await _tokenService.RevokeTokenAsync(token);

        return Ok();
    }
}

// Current User Service
public class OrderService
{
    private readonly ICurrentUserService _currentUser;

    public async Task CreateOrderAsync(CreateOrderRequest request)
    {
        var order = new Order
        {
            UserId = _currentUser.UserId, // Automatically from JWT
            UserName = _currentUser.UserName,
            TenantId = _currentUser.TenantId
        };
    }
}

📋 CQRS & MediatR

Commands
// Command Definition
public record CreateOrderCommand(
    string CustomerId,
    List<OrderItem> Items,
    string ShippingAddress
) : ICommand<Guid>;

// Command Handler
public class CreateOrderHandler : ICommandHandler<CreateOrderCommand, Guid>
{
    private readonly IRepository<Order> _orderRepository;
    private readonly ITenantContext _tenantContext;
    private readonly IEventBus _eventBus;

    public async Task<Guid> HandleAsync(CreateOrderCommand command, CancellationToken cancellationToken)
    {
        var order = new Order
        {
            CustomerId = command.CustomerId,
            Items = command.Items,
            ShippingAddress = command.ShippingAddress,
            TenantId = _tenantContext.TenantId,
            Status = OrderStatus.Created
        };

        await _orderRepository.AddAsync(order, cancellationToken);

        // Publish domain event
        await _eventBus.PublishDomainEventAsync(
            new OrderCreatedEvent(order.Id, order.CustomerId),
            cancellationToken);

        return order.Id;
    }
}
Queries
// Query Definition
public record GetOrdersByCustomerQuery(string CustomerId, int Page = 1, int PageSize = 10)
    : IQuery<PagedResult<OrderDto>>;

// Query Handler
public class GetOrdersByCustomerHandler : IQueryHandler<GetOrdersByCustomerQuery, PagedResult<OrderDto>>
{
    private readonly IRepository<Order> _orderRepository;
    private readonly ITenantContext _tenantContext;

    public async Task<PagedResult<OrderDto>> HandleAsync(GetOrdersByCustomerQuery query, CancellationToken cancellationToken)
    {
        var orders = await _orderRepository.GetPagedAsync(
            page: query.Page,
            pageSize: query.PageSize,
            predicate: o => o.CustomerId == query.CustomerId && o.TenantId == _tenantContext.TenantId,
            orderBy: o => o.CreatedDate,
            cancellationToken: cancellationToken
        );

        return orders.Map(o => new OrderDto
        {
            Id = o.Id,
            CustomerId = o.CustomerId,
            Status = o.Status.ToString(),
            CreatedDate = o.CreatedDate
        });
    }
}
Controller Usage
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly IMediator _mediator;

    [HttpPost]
    public async Task<IActionResult> CreateOrder(CreateOrderCommand command)
    {
        var orderId = await _mediator.Send(command);
        return CreatedAtAction(nameof(GetOrder), new { id = orderId }, orderId);
    }

    [HttpGet("customer/{customerId}")]
    public async Task<IActionResult> GetOrdersByCustomer(string customerId, int page = 1, int pageSize = 10)
    {
        var query = new GetOrdersByCustomerQuery(customerId, page, pageSize);
        var result = await _mediator.Send(query);
        return Ok(result);
    }
}

💰 Payment Processing

Payment Domain
// Payment Entity with Domain Events
public class Payment : BaseEntity
{
    public string OrderId { get; private set; }
    public Money Amount { get; private set; }
    public PaymentMethod Method { get; private set; }
    public PaymentStatus Status { get; private set; }
    public string? TransactionId { get; private set; }
    public string? GatewayResponse { get; private set; }

    private readonly List<DomainEvent> _domainEvents = new();
    public IReadOnlyCollection<DomainEvent> DomainEvents => _domainEvents.AsReadOnly();

    public Payment(string orderId, Money amount, PaymentMethod method)
    {
        OrderId = orderId;
        Amount = amount;
        Method = method;
        Status = PaymentStatus.Pending;

        _domainEvents.Add(new PaymentInitiatedEvent(Id, OrderId, Amount));
    }

    public void MarkAsProcessing()
    {
        if (Status != PaymentStatus.Pending)
            throw new InvalidOperationException($"Cannot process payment from status {Status}");

        Status = PaymentStatus.Processing;
    }

    public void MarkAsSuccessful(string transactionId, string? gatewayResponse = null)
    {
        if (Status != PaymentStatus.Processing)
            throw new InvalidOperationException($"Cannot complete payment from status {Status}");

        Status = PaymentStatus.Successful;
        TransactionId = transactionId;
        GatewayResponse = gatewayResponse;

        _domainEvents.Add(new PaymentCompletedEvent(Id, OrderId, TransactionId, Amount));
    }

    public void MarkAsFailed(string reason)
    {
        Status = PaymentStatus.Failed;
        GatewayResponse = reason;

        _domainEvents.Add(new PaymentFailedEvent(Id, OrderId, reason));
    }

    public Money ApplyDiscount(decimal percentage)
    {
        return Amount.ApplyDiscount(percentage);
    }

    public void ClearDomainEvents() => _domainEvents.Clear();
}

// Payment Service
public class PaymentService
{
    private readonly IRepository<Payment> _paymentRepository;
    private readonly IPaymentGateway _paymentGateway;
    private readonly IEventBus _eventBus;

    public async Task<PaymentResult> ProcessPaymentAsync(ProcessPaymentRequest request)
    {
        var payment = new Payment(
            orderId: request.OrderId,
            amount: new Money(request.Amount, request.Currency),
            method: request.PaymentMethod
        );

        payment.MarkAsProcessing();
        await _paymentRepository.AddAsync(payment);

        try
        {
            var gatewayResult = await _paymentGateway.ProcessAsync(new PaymentGatewayRequest
            {
                Amount = payment.Amount.Amount,
                Currency = payment.Amount.Currency.Code,
                PaymentMethod = payment.Method,
                OrderId = payment.OrderId
            });

            if (gatewayResult.IsSuccess)
            {
                payment.MarkAsSuccessful(gatewayResult.TransactionId, gatewayResult.Response);
            }
            else
            {
                payment.MarkAsFailed(gatewayResult.ErrorMessage);
            }

            await _paymentRepository.UpdateAsync(payment);

            // Publish domain events
            foreach (var domainEvent in payment.DomainEvents)
            {
                await _eventBus.PublishDomainEventAsync(domainEvent);
            }

            payment.ClearDomainEvents();

            return new PaymentResult
            {
                IsSuccess = payment.Status == PaymentStatus.Successful,
                PaymentId = payment.Id,
                TransactionId = payment.TransactionId,
                Status = payment.Status
            };
        }
        catch (Exception ex)
        {
            payment.MarkAsFailed(ex.Message);
            await _paymentRepository.UpdateAsync(payment);
            throw;
        }
    }
}

📦 Shipping Management

Shipping Domain
// Shipping Aggregate
public class Shipment : BaseEntity
{
    public string OrderId { get; private set; }
    public string TrackingNumber { get; private set; }
    public ShippingStatus Status { get; private set; }
    public ShippingCarrier Carrier { get; private set; }
    public Address FromAddress { get; private set; }
    public Address ToAddress { get; private set; }
    public Money ShippingCost { get; private set; }
    public decimal Weight { get; private set; }
    public DateTime? ShippedAt { get; private set; }
    public DateTime? DeliveredAt { get; private set; }
    public DateTime EstimatedDelivery { get; private set; }

    private readonly List<ShipmentItem> _items = new();
    public IReadOnlyCollection<ShipmentItem> Items => _items.AsReadOnly();

    public Shipment(string orderId, ShippingCarrier carrier, Address fromAddress,
                   Address toAddress, Money shippingCost, DateTime estimatedDelivery)
    {
        OrderId = orderId;
        Carrier = carrier;
        FromAddress = fromAddress;
        ToAddress = toAddress;
        ShippingCost = shippingCost;
        EstimatedDelivery = estimatedDelivery;
        Status = ShippingStatus.Pending;
        TrackingNumber = GenerateTrackingNumber();
    }

    public void AddItem(string productId, string productName, int quantity, decimal weight)
    {
        _items.Add(new ShipmentItem(productId, productName, quantity, weight));
        Weight += weight * quantity;
    }

    public void MarkAsShipped()
    {
        if (Status != ShippingStatus.Pending)
            throw new InvalidOperationException($"Cannot ship from status {Status}");

        Status = ShippingStatus.Shipped;
        ShippedAt = DateTime.UtcNow;
    }

    public void MarkAsDelivered(string? signedBy = null)
    {
        Status = ShippingStatus.Delivered;
        DeliveredAt = DateTime.UtcNow;
    }

    private static string GenerateTrackingNumber()
    {
        return $"TRK{DateTime.UtcNow:yyyyMMdd}{Guid.NewGuid().ToString()[..8].ToUpper()}";
    }
}

// Shipping Service
public class ShippingService
{
    private readonly IRepository<Shipment> _shipmentRepository;
    private readonly IShippingProvider _shippingProvider;

    public async Task<Shipment> CreateShipmentAsync(CreateShipmentRequest request)
    {
        var shipment = new Shipment(
            orderId: request.OrderId,
            carrier: request.Carrier,
            fromAddress: request.FromAddress,
            toAddress: request.ToAddress,
            shippingCost: new Money(request.ShippingCost, Currency.USD),
            estimatedDelivery: request.EstimatedDelivery
        );

        foreach (var item in request.Items)
        {
            shipment.AddItem(item.ProductId, item.ProductName, item.Quantity, item.Weight);
        }

        await _shipmentRepository.AddAsync(shipment);

        // Create shipping label with carrier
        var label = await _shippingProvider.CreateLabelAsync(new ShippingLabelRequest
        {
            TrackingNumber = shipment.TrackingNumber,
            FromAddress = shipment.FromAddress,
            ToAddress = shipment.ToAddress,
            Weight = shipment.Weight
        });

        return shipment;
    }

    public async Task<TrackingInfo> TrackShipmentAsync(string trackingNumber)
    {
        var shipment = await _shipmentRepository.GetByExpressionAsync(s => s.TrackingNumber == trackingNumber);
        if (shipment == null)
            throw new NotFoundException("Shipment not found");

        var trackingInfo = await _shippingProvider.GetTrackingInfoAsync(trackingNumber);

        // Update shipment status based on tracking info
        if (trackingInfo.Status == "DELIVERED" && shipment.Status != ShippingStatus.Delivered)
        {
            shipment.MarkAsDelivered();
            await _shipmentRepository.UpdateAsync(shipment);
        }

        return trackingInfo;
    }
}

🔄 Saga Patterns

Order Processing Saga
// Saga State
public class OrderSagaState : SagaStateMachineInstance
{
    public Guid CorrelationId { get; set; }
    public string? CurrentState { get; set; }
    public string? OrderId { get; set; }
    public string? CustomerId { get; set; }
    public decimal TotalAmount { get; set; }
    public bool PaymentProcessed { get; set; }
    public bool InventoryReserved { get; set; }
    public bool ShippingArranged { get; set; }
    public DateTime OrderDate { get; set; }
}

// Saga State Machine
public class OrderSagaStateMachine : MassTransitStateMachine<OrderSagaState>
{
    public State Processing { get; private set; }
    public State PaymentPending { get; private set; }
    public State InventoryPending { get; private set; }
    public State ShippingPending { get; private set; }
    public State Completed { get; private set; }
    public State Failed { get; private set; }

    public Event<IOrderSubmitted> OrderSubmitted { get; private set; }
    public Event<IPaymentProcessed> PaymentProcessed { get; private set; }
    public Event<IInventoryReserved> InventoryReserved { get; private set; }

    public OrderSagaStateMachine()
    {
        InstanceState(x => x.CurrentState);

        Event(() => OrderSubmitted, x => x.CorrelateById(context => context.Message.OrderId));
        Event(() => PaymentProcessed, x => x.CorrelateById(context => context.Message.OrderId));
        Event(() => InventoryReserved, x => x.CorrelateById(context => context.Message.OrderId));

        Initially(
            When(OrderSubmitted)
                .Then(context =>
                {
                    context.Saga.OrderId = context.Message.OrderId.ToString();
                    context.Saga.CustomerId = context.Message.CustomerId;
                    context.Saga.TotalAmount = context.Message.TotalAmount;
                    context.Saga.OrderDate = DateTime.UtcNow;
                })
                .TransitionTo(Processing)
                .Publish(context => new ProcessPaymentCommand
                {
                    OrderId = context.Message.OrderId,
                    Amount = context.Message.TotalAmount,
                    CustomerId = context.Message.CustomerId
                }));

        During(Processing,
            When(PaymentProcessed)
                .Then(context => context.Saga.PaymentProcessed = true)
                .TransitionTo(PaymentPending)
                .Publish(context => new ReserveInventoryCommand
                {
                    OrderId = Guid.Parse(context.Saga.OrderId!)
                }));

        During(PaymentPending,
            When(InventoryReserved)
                .Then(context => context.Saga.InventoryReserved = true)
                .TransitionTo(InventoryPending)
                .Publish(context => new ArrangeShippingCommand
                {
                    OrderId = Guid.Parse(context.Saga.OrderId!)
                }));
    }
}

📨 Messaging & Event Streaming

RabbitMQ + MassTransit Configuration
// Configuration
{
  "RabbitMQ": {
    "Host": "localhost",
    "Port": 5672,
    "Username": "guest",
    "Password": "guest",
    "VirtualHost": "/",
    "RetryLimit": 3,
    "RetryInterval": "00:00:30"
  }
}

// Service Registration
services.AddMassTransit(x =>
{
    x.AddSagaStateMachine<OrderSagaStateMachine, OrderSagaState>()
        .InMemoryRepository();

    x.AddConsumer<OrderCreatedEventHandler>();
    x.AddConsumer<PaymentProcessedEventHandler>();

    x.UsingRabbitMq((context, cfg) =>
    {
        cfg.Host(configuration["RabbitMQ:Host"], h =>
        {
            h.Username(configuration["RabbitMQ:Username"]);
            h.Password(configuration["RabbitMQ:Password"]);
        });

        cfg.ConfigureEndpoints(context);

        // Configure retry policy
        cfg.UseMessageRetry(r => r.Interval(3, TimeSpan.FromSeconds(30)));
    });
});
Kafka Configuration
// Configuration
{
  "Kafka": {
    "BootstrapServers": "localhost:9092",
    "GroupId": "marventa-consumers",
    "AutoOffsetReset": "Earliest",
    "EnableAutoCommit": false
  }
}

// Kafka Producer
public class KafkaMessageBus : IMessageBus
{
    private readonly IProducer<string, string> _producer;
    private readonly KafkaOptions _options;

    public async Task PublishEventAsync<T>(T eventMessage, string topic = null) where T : IIntegrationEvent
    {
        topic ??= typeof(T).Name.ToLowerInvariant();

        var message = new Message<string, string>
        {
            Key = eventMessage.CorrelationId,
            Value = JsonSerializer.Serialize(eventMessage),
            Headers = new Headers
            {
                { "tenant-id", Encoding.UTF8.GetBytes(_tenantContext.TenantId ?? "global") },
                { "event-type", Encoding.UTF8.GetBytes(typeof(T).Name) },
                { "timestamp", Encoding.UTF8.GetBytes(DateTime.UtcNow.ToString("O")) }
            }
        };

        await _producer.ProduceAsync(topic, message);
    }
}

// Kafka Consumer
public abstract class BaseKafkaHandler<T> : IKafkaHandler<T> where T : IIntegrationEvent
{
    private readonly ILogger<BaseKafkaHandler<T>> _logger;
    private readonly ITenantContext _tenantContext;

    public async Task HandleAsync(ConsumeResult<string, string> consumeResult, CancellationToken cancellationToken)
    {
        try
        {
            // Extract tenant context from headers
            if (consumeResult.Message.Headers.TryGetLastBytes("tenant-id", out var tenantIdBytes))
            {
                var tenantId = Encoding.UTF8.GetString(tenantIdBytes);
                _tenantContext.SetTenant(tenantId);
            }

            var eventMessage = JsonSerializer.Deserialize<T>(consumeResult.Message.Value);
            await ProcessEventAsync(eventMessage, cancellationToken);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error processing Kafka message: {Topic} {Partition} {Offset}",
                consumeResult.Topic, consumeResult.Partition, consumeResult.Offset);
            throw;
        }
    }

    protected abstract Task ProcessEventAsync(T eventMessage, CancellationToken cancellationToken);
}
Event Handlers
// RabbitMQ Event Handler
public class OrderCreatedEventHandler : IDomainEventHandler<OrderCreatedEvent>
{
    private readonly ILogger<OrderCreatedEventHandler> _logger;
    private readonly IEventBus _eventBus;

    public async Task HandleAsync(OrderCreatedEvent domainEvent, CancellationToken cancellationToken = default)
    {
        _logger.LogInformation("Processing order created event for order {OrderId} in tenant {TenantId}",
            domainEvent.OrderId, _tenantContext.TenantId);

        // Convert to integration event for external systems
        var integrationEvent = new OrderCreatedIntegrationEvent
        {
            OrderId = domainEvent.OrderId,
            CustomerId = domainEvent.CustomerId,
            CorrelationId = Guid.NewGuid().ToString(),
            OccurredOn = DateTime.UtcNow,
            TenantId = _tenantContext.TenantId
        };

        await _eventBus.PublishIntegrationEventAsync(integrationEvent, cancellationToken);
    }
}

// Kafka Event Handler
public class PaymentProcessedKafkaHandler : BaseKafkaHandler<PaymentProcessedIntegrationEvent>
{
    private readonly IMediator _mediator;

    protected override async Task ProcessEventAsync(PaymentProcessedIntegrationEvent eventMessage, CancellationToken cancellationToken)
    {
        var command = new UpdateOrderPaymentStatusCommand(
            eventMessage.OrderId,
            PaymentStatus.Completed,
            eventMessage.TransactionId
        );

        await _mediator.Send(command, cancellationToken);
    }
}

📤 Outbox/Inbox Patterns

Outbox Implementation
// Outbox Message
public class OutboxMessage : BaseEntity
{
    public string Type { get; set; }
    public string Data { get; set; }
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
    public DateTime? ProcessedAt { get; set; }
    public string? Error { get; set; }
    public int RetryCount { get; set; } = 0;
    public string? IdempotencyKey { get; set; }

    public bool IsProcessed => ProcessedAt.HasValue;
    public bool HasFailed => !string.IsNullOrEmpty(Error);
    public bool ShouldRetry => RetryCount < 3 && !IsProcessed;

    public static OutboxMessage Create<T>(T message, string? idempotencyKey = null) where T : class
    {
        return new OutboxMessage
        {
            Type = typeof(T).Name,
            Data = JsonSerializer.Serialize(message),
            IdempotencyKey = idempotencyKey
        };
    }
}

// Outbox Service
public class OutboxService : IOutboxService
{
    private readonly IRepository<OutboxMessage> _outboxRepository;

    public async Task SaveMessageAsync<T>(T message, string? idempotencyKey = null) where T : class
    {
        var outboxMessage = OutboxMessage.Create(message, idempotencyKey);
        await _outboxRepository.AddAsync(outboxMessage);
    }

    public async Task<List<OutboxMessage>> GetUnprocessedMessagesAsync(int batchSize = 100)
    {
        return await _outboxRepository.GetListAsync(
            predicate: m => !m.IsProcessed && m.ShouldRetry,
            orderBy: m => m.CreatedAt,
            take: batchSize
        );
    }

    public async Task MarkAsProcessedAsync(Guid messageId)
    {
        var message = await _outboxRepository.GetByIdAsync(messageId);
        if (message != null)
        {
            message.ProcessedAt = DateTime.UtcNow;
            await _outboxRepository.UpdateAsync(message);
        }
    }
}

// Background Outbox Dispatcher
public class OutboxDispatcher : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<OutboxDispatcher> _logger;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                using var scope = _serviceProvider.CreateScope();
                var outboxService = scope.ServiceProvider.GetRequiredService<IOutboxService>();
                var eventBus = scope.ServiceProvider.GetRequiredService<IEventBus>();

                var messages = await outboxService.GetUnprocessedMessagesAsync();

                foreach (var message in messages)
                {
                    try
                    {
                        // Deserialize and publish the message
                        var eventType = Type.GetType($"YourApp.Events.{message.Type}");
                        var @event = JsonSerializer.Deserialize(message.Data, eventType!);

                        await eventBus.PublishIntegrationEventAsync(@event as IIntegrationEvent);
                        await outboxService.MarkAsProcessedAsync(message.Id);
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError(ex, "Failed to process outbox message {MessageId}", message.Id);
                        // Implement retry logic
                    }
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error in outbox dispatcher");
            }

            await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
        }
    }
}

💵 Money/Currency Value Objects

// Currency Value Object
public class Currency : ValueObject
{
    public static readonly Currency USD = new("USD", "$", 2);
    public static readonly Currency EUR = new("EUR", "€", 2);
    public static readonly Currency GBP = new("GBP", "£", 2);
    public static readonly Currency JPY = new("JPY", "¥", 0);
    public static readonly Currency TRY = new("TRY", "₺", 2);

    public string Code { get; }
    public string Symbol { get; }
    public int DecimalPlaces { get; }

    private Currency(string code, string symbol, int decimalPlaces)
    {
        Code = code;
        Symbol = symbol;
        DecimalPlaces = decimalPlaces;
    }

    public static Currency FromCode(string code)
    {
        return code.ToUpper() switch
        {
            "USD" => USD,
            "EUR" => EUR,
            "GBP" => GBP,
            "JPY" => JPY,
            "TRY" => TRY,
            _ => throw new ArgumentException($"Unsupported currency: {code}")
        };
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return Code;
    }
}

// Money Value Object
public class Money : ValueObject
{
    public decimal Amount { get; }
    public Currency Currency { get; }

    public Money(decimal amount, Currency currency)
    {
        Amount = Math.Round(amount, currency.DecimalPlaces);
        Currency = currency ?? throw new ArgumentNullException(nameof(currency));
    }

    // Arithmetic Operations
    public static Money operator +(Money left, Money right)
    {
        if (left.Currency != right.Currency)
            throw new InvalidOperationException("Cannot add money with different currencies");

        return new Money(left.Amount + right.Amount, left.Currency);
    }

    public static Money operator -(Money left, Money right)
    {
        if (left.Currency != right.Currency)
            throw new InvalidOperationException("Cannot subtract money with different currencies");

        return new Money(left.Amount - right.Amount, left.Currency);
    }

    public static Money operator *(Money money, decimal factor)
    {
        return new Money(money.Amount * factor, money.Currency);
    }

    // Business Operations
    public Money ApplyTax(decimal taxRate)
    {
        return new Money(Amount * (1 + taxRate), Currency);
    }

    public Money ApplyDiscount(decimal discountRate)
    {
        if (discountRate < 0 || discountRate > 1)
            throw new ArgumentOutOfRangeException(nameof(discountRate));

        return new Money(Amount * (1 - discountRate), Currency);
    }

    public Money ConvertTo(Currency targetCurrency, decimal exchangeRate)
    {
        return new Money(Amount * exchangeRate, targetCurrency);
    }

    public string Format()
    {
        return $"{Currency.Symbol}{Amount:N2}";
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return Amount;
        yield return Currency;
    }
}

// Usage Examples
public class OrderService
{
    public async Task<Order> CalculateOrderTotalAsync(List<OrderItem> items, decimal taxRate, decimal discountRate)
    {
        var subtotal = Money.Zero(Currency.USD);

        foreach (var item in items)
        {
            var itemTotal = new Money(item.UnitPrice, Currency.USD) * item.Quantity;
            subtotal += itemTotal;
        }

        var discounted = subtotal.ApplyDiscount(discountRate);
        var total = discounted.ApplyTax(taxRate);

        return new Order
        {
            Items = items,
            Subtotal = subtotal,
            DiscountAmount = subtotal - discounted,
            TaxAmount = total - discounted,
            Total = total
        };
    }
}

🗄️ Caching (Redis & In-Memory)

Redis Distributed Caching
// Configuration
{
  "Redis": {
    "ConnectionString": "localhost:6379",
    "DatabaseId": 0,
    "KeyPrefix": "MyApp",
    "DefaultExpiration": "00:05:00"
  }
}

// Service Registration
services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = configuration.GetConnectionString("Redis");
});
In-Memory Caching
// Configuration
services.AddMemoryCache(options =>
{
    options.SizeLimit = 1024; // 1GB
    options.CompactionPercentage = 0.25;
});
Tenant-Scoped Caching
public class TenantScopedCache : ITenantScopedCache
{
    private readonly IDistributedCache _distributedCache;
    private readonly ITenantContext _tenantContext;

    public async Task<T?> GetAsync<T>(string key, CancellationToken cancellationToken = default)
    {
        var tenantKey = GetTenantScopedKey(key);
        var cached = await _distributedCache.GetStringAsync(tenantKey, cancellationToken);

        if (string.IsNullOrEmpty(cached))
            return default;

        return JsonSerializer.Deserialize<T>(cached);
    }

    public async Task SetAsync<T>(string key, T value, TimeSpan? expiration = null, CancellationToken cancellationToken = default)
    {
        var tenantKey = GetTenantScopedKey(key);
        var json = JsonSerializer.Serialize(value);

        var options = new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = expiration ?? TimeSpan.FromMinutes(5)
        };

        await _distributedCache.SetStringAsync(tenantKey, json, options, cancellationToken);
    }

    private string GetTenantScopedKey(string key)
    {
        var tenantId = _tenantContext.TenantId ?? "global";
        return $"tenant:{tenantId}:{key}";
    }
}

// Usage
public class ProductService
{
    private readonly ITenantScopedCache _cache;
    private readonly IMemoryCache _memoryCache;
    private readonly IRepository<Product> _productRepository;

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

        // Try memory cache first (fastest)
        if (_memoryCache.TryGetValue(cacheKey, out Product? cachedProduct))
            return cachedProduct;

        // Try distributed cache (tenant-scoped)
        cachedProduct = await _cache.GetAsync<Product>(cacheKey);
        if (cachedProduct != null)
        {
            // Store in memory cache for 2 minutes
            _memoryCache.Set(cacheKey, cachedProduct, TimeSpan.FromMinutes(2));
            return cachedProduct;
        }

        // Get from database
        var product = await _productRepository.GetByIdAsync(productId);
        if (product != null)
        {
            // Cache in both levels
            await _cache.SetAsync(cacheKey, product, TimeSpan.FromMinutes(10));
            _memoryCache.Set(cacheKey, product, TimeSpan.FromMinutes(2));
        }

        return product;
    }
}

🚦 Rate Limiting

// Configuration
public class RateLimitOptions
{
    public bool EnableRateLimiting { get; set; } = true;
    public int MaxRequests { get; set; } = 100;
    public TimeSpan WindowSize { get; set; } = TimeSpan.FromMinutes(1);
    public Dictionary<string, EndpointRateLimit> EndpointLimits { get; set; } = new();
}

public class EndpointRateLimit
{
    public int MaxRequests { get; set; }
    public TimeSpan WindowSize { get; set; }
}

// Tenant-Aware Rate Limiter
public class TenantRateLimiter : ITenantRateLimiter
{
    private readonly ICacheService _cache;
    private readonly ITenantContext _tenantContext;

    public async Task<RateLimitResult> CheckRateLimitAsync(string endpoint, CancellationToken cancellationToken = default)
    {
        var tenantId = _tenantContext.TenantId ?? "global";
        var key = $"rate_limit:{tenantId}:{endpoint}";

        var currentCount = await _cache.GetAsync<int?>(key) ?? 0;
        var maxRequests = GetMaxRequestsForEndpoint(endpoint);
        var windowSize = GetWindowSizeForEndpoint(endpoint);

        if (currentCount >= maxRequests)
        {
            return new RateLimitResult
            {
                IsAllowed = false,
                Remaining = 0,
                ResetTime = DateTimeOffset.UtcNow.Add(windowSize)
            };
        }

        await _cache.SetAsync(key, currentCount + 1, windowSize);

        return new RateLimitResult
        {
            IsAllowed = true,
            Remaining = maxRequests - currentCount - 1,
            ResetTime = DateTimeOffset.UtcNow.Add(windowSize)
        };
    }
}

// Rate Limit Attribute
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class RateLimitAttribute : Attribute
{
    public int MaxRequests { get; set; } = 100;
    public int WindowSizeInSeconds { get; set; } = 60;
}

// Usage
[RateLimit(MaxRequests = 50, WindowSizeInSeconds = 60)]
public class ProductsController : ControllerBase
{
    [HttpGet]
    [RateLimit(MaxRequests = 20, WindowSizeInSeconds = 60)] // Override for specific endpoint
    public async Task<IActionResult> GetProducts()
    {
        // This endpoint is limited to 20 requests per minute per tenant
        return Ok(await _productService.GetProductsAsync());
    }
}

🏥 Health Checks

// Database Health Check
public class DatabaseHealthCheck : IHealthCheck
{
    private readonly IConnectionFactory _connectionFactory;

    public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        try
        {
            using var connection = await _connectionFactory.CreateConnectionAsync();
            await connection.OpenAsync(cancellationToken);

            using var command = connection.CreateCommand();
            command.CommandText = "SELECT 1";
            await command.ExecuteScalarAsync(cancellationToken);

            return HealthCheckResult.Healthy("Database is responsive");
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy("Database is not responsive", ex);
        }
    }
}

// Tenant Health Check
public class TenantHealthCheck : IHealthCheck
{
    private readonly ITenantStore _tenantStore;
    private readonly ITenantContext _tenantContext;

    public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        try
        {
            var tenants = await _tenantStore.GetAllAsync();
            var activeTenants = tenants.Count();

            var data = new Dictionary<string, object>
            {
                ["total_tenants"] = activeTenants,
                ["current_tenant"] = _tenantContext.TenantId ?? "none"
            };

            return HealthCheckResult.Healthy($"Multi-tenancy system is healthy with {activeTenants} active tenants", data);
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy("Multi-tenancy system is not healthy", ex);
        }
    }
}

// Registration
builder.Services.AddHealthChecks()
    .AddCheck<DatabaseHealthCheck>("database")
    .AddCheck<TenantHealthCheck>("tenancy")
    .AddCheck<CacheHealthCheck>("cache")
    .AddCheck("external-api", () => HealthCheckResult.Healthy("External API is responsive"));

// Health Check Controller
[ApiController]
[Route("api/[controller]")]
public class HealthController : ControllerBase
{
    private readonly HealthCheckService _healthCheckService;

    [HttpGet]
    public async Task<IActionResult> Get()
    {
        var report = await _healthCheckService.CheckHealthAsync();

        var result = new
        {
            Status = report.Status.ToString(),
            TotalDuration = report.TotalDuration,
            Entries = report.Entries.Select(e => new
            {
                Name = e.Key,
                Status = e.Value.Status.ToString(),
                Duration = e.Value.Duration,
                Description = e.Value.Description,
                Data = e.Value.Data
            })
        };

        return report.Status == HealthStatus.Healthy ? Ok(result) : StatusCode(503, result);
    }
}

📊 API Versioning

// Configuration
public class ApiVersioningOptions
{
    public string DefaultVersion { get; set; } = "1.0";
    public bool AssumeDefaultVersionWhenUnspecified { get; set; } = true;
    public ApiVersionReader ApiVersionReader { get; set; } = ApiVersionReader.Header;
    public string HeaderName { get; set; } = "X-Api-Version";
    public string QueryParameterName { get; set; } = "version";
}

// Versioned Controller
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    [MapToApiVersion("1.0")]
    public async Task<IActionResult> GetProductsV1()
    {
        // Version 1.0 implementation
        var products = await _productService.GetProductsAsync();
        return Ok(products.Select(p => new ProductV1Dto
        {
            Id = p.Id,
            Name = p.Name,
            Price = p.Price.Amount
        }));
    }

    [HttpGet]
    [MapToApiVersion("2.0")]
    public async Task<IActionResult> GetProductsV2()
    {
        // Version 2.0 implementation with additional fields
        var products = await _productService.GetProductsAsync();
        return Ok(products.Select(p => new ProductV2Dto
        {
            Id = p.Id,
            Name = p.Name,
            Price = p.Price,
            Currency = p.Price.Currency.Code,
            CreatedDate = p.CreatedDate,
            Tags = p.Tags
        }));
    }
}

// Version-specific DTOs
public class ProductV1Dto
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

public class ProductV2Dto
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public Money Price { get; set; }
    public string Currency { get; set; }
    public DateTime CreatedDate { get; set; }
    public List<string> Tags { get; set; }
}

🔒 Security & Encryption

// Encryption Service
public class EncryptionService : IEncryptionService
{
    private readonly IDataProtector _dataProtector;

    public EncryptionService(IDataProtectionProvider dataProtectionProvider)
    {
        _dataProtector = dataProtectionProvider.CreateProtector("Marventa.Framework.Encryption");
    }

    public string Encrypt(string plainText)
    {
        if (string.IsNullOrEmpty(plainText))
            return plainText;

        return _dataProtector.Protect(plainText);
    }

    public string Decrypt(string cipherText)
    {
        if (string.IsNullOrEmpty(cipherText))
            return cipherText;

        try
        {
            return _dataProtector.Unprotect(cipherText);
        }
        catch
        {
            throw new InvalidOperationException("Unable to decrypt data");
        }
    }

    public string Hash(string input, string salt = null)
    {
        salt ??= GenerateSalt();
        using var sha256 = SHA256.Create();
        var hashedBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(input + salt));
        return Convert.ToBase64String(hashedBytes);
    }

    private static string GenerateSalt()
    {
        var salt = new byte[16];
        using var rng = RandomNumberGenerator.Create();
        rng.GetBytes(salt);
        return Convert.ToBase64String(salt);
    }
}

// Sensitive Data Attribute
[AttributeUsage(AttributeTargets.Property)]
public class SensitiveDataAttribute : Attribute
{
    public bool ShouldEncrypt { get; set; } = true;
    public bool ShouldHash { get; set; } = false;
}

// Entity with Sensitive Data
public class Customer : BaseEntity
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    [SensitiveData(ShouldEncrypt = true)]
    public string Email { get; set; }

    [SensitiveData(ShouldEncrypt = true)]
    public string PhoneNumber { get; set; }

    [SensitiveData(ShouldHash = true)]
    public string SocialSecurityNumber { get; set; }
}

// Automatic Encryption Interceptor
public class EncryptionInterceptor : IEntityInterceptor
{
    private readonly IEncryptionService _encryptionService;

    public void BeforeSaving(object entity)
    {
        var properties = entity.GetType().GetProperties()
            .Where(p => p.GetCustomAttribute<SensitiveDataAttribute>() != null);

        foreach (var property in properties)
        {
            var attribute = property.GetCustomAttribute<SensitiveDataAttribute>();
            var value = property.GetValue(entity) as string;

            if (!string.IsNullOrEmpty(value))
            {
                if (attribute.ShouldEncrypt)
                {
                    property.SetValue(entity, _encryptionService.Encrypt(value));
                }
                else if (attribute.ShouldHash)
                {
                    property.SetValue(entity, _encryptionService.Hash(value));
                }
            }
        }
    }

    public void AfterLoading(object entity)
    {
        var properties = entity.GetType().GetProperties()
            .Where(p => p.GetCustomAttribute<SensitiveDataAttribute>()?.ShouldEncrypt == true);

        foreach (var property in properties)
        {
            var value = property.GetValue(entity) as string;
            if (!string.IsNullOrEmpty(value))
            {
                try
                {
                    property.SetValue(entity, _encryptionService.Decrypt(value));
                }
                catch
                {
                    // Handle decryption failure
                }
            }
        }
    }
}

📧 Communication Services

// Email Service
public class EmailService : IEmailService
{
    private readonly ISmtpClient _smtpClient;
    private readonly EmailOptions _options;
    private readonly ITenantContext _tenantContext;

    public async Task SendEmailAsync(EmailMessage message, CancellationToken cancellationToken = default)
    {
        var mailMessage = new MailMessage
        {
            From = new MailAddress(_options.FromEmail, _options.FromName),
            Subject = message.Subject,
            Body = message.Body,
            IsBodyHtml = message.IsHtml
        };

        foreach (var recipient in message.To)
        {
            mailMessage.To.Add(recipient);
        }

        // Add tenant-specific headers
        if (!string.IsNullOrEmpty(_tenantContext.TenantId))
        {
            mailMessage.Headers.Add("X-Tenant-Id", _tenantContext.TenantId);
        }

        try
        {
            await _smtpClient.SendAsync(mailMessage, cancellationToken);
        }
        catch (Exception ex)
        {
            throw new EmailDeliveryException($"Failed to send email: {ex.Message}", ex);
        }
    }

    public async Task SendTemplatedEmailAsync<T>(string templateName, T model, string[] recipients, CancellationToken cancellationToken = default) where T : class
    {
        var template = await LoadTemplateAsync(templateName);
        var body = await RenderTemplateAsync(template, model);

        var message = new EmailMessage
        {
            To = recipients,
            Subject = template.Subject,
            Body = body,
            IsHtml = true
        };

        await SendEmailAsync(message, cancellationToken);
    }
}

// SMS Service
public class SmsService : ISmsService
{
    private readonly ISmsProvider _smsProvider;
    private readonly SmsOptions _options;

    public async Task SendSmsAsync(SmsMessage message, CancellationToken cancellationToken = default)
    {
        try
        {
            await _smsProvider.SendAsync(new SmsRequest
            {
                To = message.PhoneNumber,
                Message = message.Text,
                From = _options.FromNumber
            }, cancellationToken);
        }
        catch (Exception ex)
        {
            throw new SmsDeliveryException($"Failed to send SMS: {ex.Message}", ex);
        }
    }

    public async Task SendBulkSmsAsync(BulkSmsMessage message, CancellationToken cancellationToken = default)
    {
        var tasks = message.PhoneNumbers.Select(phoneNumber =>
            SendSmsAsync(new SmsMessage { PhoneNumber = phoneNumber, Text = message.Text }, cancellationToken)
        );

        await Task.WhenAll(tasks);
    }
}

// Usage Example
public class OrderNotificationService
{
    private readonly IEmailService _emailService;
    private readonly ISmsService _smsService;

    public async Task NotifyOrderCreatedAsync(Order order)
    {
        // Send email notification
        await _emailService.SendTemplatedEmailAsync("order-created", new
        {
            OrderId = order.Id,
            CustomerName = order.CustomerName,
            Total = order.Total.Format(),
            Items = order.Items
        }, new[] { order.CustomerEmail });

        // Send SMS notification if phone number is provided
        if (!string.IsNullOrEmpty(order.CustomerPhone))
        {
            await _smsService.SendSmsAsync(new SmsMessage
            {
                PhoneNumber = order.CustomerPhone,
                Text = $"Your order #{order.Id} has been created successfully. Total: {order.Total.Format()}"
            });
        }
    }
}

⚡ Circuit Breaker Pattern

// Circuit Breaker Implementation
public class CircuitBreakerHandler : DelegatingHandler
{
    private readonly CircuitBreakerOptions _options;
    private readonly ILogger<CircuitBreakerHandler> _logger;
    private readonly object _lock = new();
    private CircuitState _state = CircuitState.Closed;
    private int _failureCount = 0;
    private DateTime _lastFailureTime = DateTime.MinValue;

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (_state == CircuitState.Open)
        {
            if (DateTime.UtcNow - _lastFailureTime < _options.OpenTimeout)
            {
                throw new CircuitBreakerOpenException("Circuit breaker is open");
            }

            _state = CircuitState.HalfOpen;
        }

        try
        {
            var response = await base.SendAsync(request, cancellationToken);

            if (IsSuccessStatusCode(response.StatusCode))
            {
                OnSuccess();
                return response;
            }

            OnFailure();
            return response;
        }
        catch (Exception)
        {
            OnFailure();
            throw;
        }
    }

    private void OnSuccess()
    {
        lock (_lock)
        {
            _failureCount = 0;
            _state = CircuitState.Closed;
        }
    }

    private void OnFailure()
    {
        lock (_lock)
        {
            _failureCount++;
            _lastFailureTime = DateTime.UtcNow;

            if (_failureCount >= _options.FailureThreshold)
            {
                _state = CircuitState.Open;
                _logger.LogWarning("Circuit breaker opened after {FailureCount} failures", _failureCount);
            }
        }
    }

    private static bool IsSuccessStatusCode(HttpStatusCode statusCode)
    {
        return (int)statusCode >= 200 && (int)statusCode <= 299;
    }
}

// Usage with HttpClient
public class ExternalApiService
{
    private readonly HttpClient _httpClient;
    private readonly ILogger<ExternalApiService> _logger;

    public ExternalApiService(HttpClient httpClient, ILogger<ExternalApiService> logger)
    {
        _httpClient = httpClient;
        _logger = logger;
    }

    public async Task<ApiResponse<T>> GetAsync<T>(string endpoint)
    {
        try
        {
            var response = await _httpClient.GetAsync(endpoint);

            if (response.IsSuccessStatusCode)
            {
                var content = await response.Content.ReadAsStringAsync();
                var data = JsonSerializer.Deserialize<T>(content);
                return ApiResponse<T>.SuccessResult(data);
            }

            return ApiResponse<T>.FailureResult($"API call failed with status: {response.StatusCode}");
        }
        catch (CircuitBreakerOpenException ex)
        {
            _logger.LogWarning("Circuit breaker is open: {Message}", ex.Message);
            return ApiResponse<T>.FailureResult("Service temporarily unavailable");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error calling external API");
            return ApiResponse<T>.FailureResult(ex.Message);
        }
    }
}

// Registration
builder.Services.AddHttpClient<ExternalApiService>(client =>
{
    client.BaseAddress = new Uri("https://api.example.com/");
    client.Timeout = TimeSpan.FromSeconds(30);
})
.AddHttpMessageHandler<CircuitBreakerHandler>();

builder.Services.Configure<CircuitBreakerOptions>(options =>
{
    options.FailureThreshold = 5;
    options.OpenTimeout = TimeSpan.FromMinutes(1);
});

🎛️ Feature Flags

// Feature Flag Service
public class FeatureFlagService : IFeatureFlagService
{
    private readonly IConfiguration _configuration;
    private readonly ITenantContext _tenantContext;
    private readonly ICacheService _cache;

    public async Task<bool> IsEnabledAsync(string featureName, CancellationToken cancellationToken = default)
    {
        // Check tenant-specific feature flags first
        if (_tenantContext.HasTenant)
        {
            var tenantFlag = await GetTenantFeatureFlagAsync(featureName, _tenantContext.TenantId!, cancellationToken);
            if (tenantFlag.HasValue)
                return tenantFlag.Value;
        }

        // Fallback to global feature flags
        var globalFlag = await GetGlobalFeatureFlagAsync(featureName, cancellationToken);
        return globalFlag;
    }

    public async Task<T> GetFeatureValueAsync<T>(string featureName, T defaultValue, CancellationToken cancellationToken = default)
    {
        try
        {
            var cacheKey = $"feature_value:{_tenantContext.TenantId}:{featureName}";
            var cachedValue = await _cache.GetAsync<T>(cacheKey, cancellationToken);

            if (cachedValue != null)
                return cachedValue;

            var configValue = _configuration[$"FeatureFlags:{featureName}:Value"];
            if (!string.IsNullOrEmpty(configValue))
            {
                var convertedValue = (T)Convert.ChangeType(configValue, typeof(T));
                await _cache.SetAsync(cacheKey, convertedValue, TimeSpan.FromMinutes(5), cancellationToken);
                return convertedValue;
            }

            return defaultValue;
        }
        catch
        {
            return defaultValue;
        }
    }

    private async Task<bool?> GetTenantFeatureFlagAsync(string featureName, string tenantId, CancellationToken cancellationToken)
    {
        var cacheKey = $"tenant_feature:{tenantId}:{featureName}";
        return await _cache.GetAsync<bool?>(cacheKey, cancellationToken);
    }

    private async Task<bool> GetGlobalFeatureFlagAsync(string featureName, CancellationToken cancellationToken)
    {
        var cacheKey = $"global_feature:{featureName}";
        var cachedValue = await _cache.GetAsync<bool?>(cacheKey, cancellationToken);

        if (cachedValue.HasValue)
            return cachedValue.Value;

        var configValue = _configuration.GetValue<bool>($"FeatureFlags:{featureName}:Enabled");
        await _cache.SetAsync(cacheKey, configValue, TimeSpan.FromMinutes(10), cancellationToken);

        return configValue;
    }
}

// Feature Flag Attribute
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class FeatureFlagAttribute : Attribute, IAsyncActionFilter
{
    private readonly string _featureName;

    public FeatureFlagAttribute(string featureName)
    {
        _featureName = featureName;
    }

    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        var featureFlagService = context.HttpContext.RequestServices.GetRequiredService<IFeatureFlagService>();

        if (await featureFlagService.IsEnabledAsync(_featureName))
        {
            await next();
        }
        else
        {
            context.Result = new NotFoundResult();
        }
    }
}

// Usage
[FeatureFlag("NewCheckoutProcess")]
public class CheckoutController : ControllerBase
{
    private readonly IFeatureFlagService _featureFlags;

    [HttpPost("process")]
    public async Task<IActionResult> ProcessCheckout(CheckoutRequest request)
    {
        // Feature is automatically checked by attribute

        var discountEnabled = await _featureFlags.IsEnabledAsync("DiscountCalculation");
        var maxDiscount = await _featureFlags.GetFeatureValueAsync("MaxDiscountPercentage", 0.1m);

        if (discountEnabled)
        {
            // Apply discount logic with feature-controlled max discount
            request.DiscountPercentage = Math.Min(request.DiscountPercentage, maxDiscount);
        }

        return Ok();
    }
}

// Configuration
{
  "FeatureFlags": {
    "NewCheckoutProcess": {
      "Enabled": true,
      "Description": "New checkout process with enhanced validation"
    },
    "DiscountCalculation": {
      "Enabled": false,
      "Description": "Advanced discount calculation"
    },
    "MaxDiscountPercentage": {
      "Value": "0.15",
      "Description": "Maximum discount percentage allowed"
    }
  }
}

📝 Validation with FluentValidation

// Base Validator
public abstract class BaseValidator<T> : AbstractValidator<T> where T : class
{
    protected void RuleForMoney(Expression<Func<T, Money>> expression)
    {
        RuleFor(expression)
            .NotNull().WithMessage("Money amount is required")
            .Must(money => money.Amount >= 0).WithMessage("Money amount must be non-negative");
    }

    protected void RuleForEmail(Expression<Func<T, string>> expression)
    {
        RuleFor(expression)
            .NotEmpty().WithMessage("Email is required")
            .EmailAddress().WithMessage("Email format is invalid")
            .MaximumLength(100).WithMessage("Email is too long");
    }

    protected void RuleForPhoneNumber(Expression<Func<T, string>> expression)
    {
        RuleFor(expression)
            .Matches(@"^\+?[1-9]\d{1,14}$").WithMessage("Phone number format is invalid");
    }

    protected void RuleForTenantId(Expression<Func<T, string>> expression)
    {
        RuleFor(expression)
            .NotEmpty().WithMessage("Tenant ID is required")
            .Length(1, 50).WithMessage("Tenant ID must be between 1 and 50 characters");
    }
}

// Command Validator
public class CreateOrderValidator : BaseValidator<CreateOrderCommand>
{
    public CreateOrderValidator()
    {
        RuleFor(x => x.CustomerId)
            .NotEmpty().WithMessage("Customer ID is required");

        RuleFor(x => x.Items)
            .NotEmpty().WithMessage("Order must contain at least one item")
            .Must(items => items.Count <= 100).WithMessage("Order cannot contain more than 100 items");

        RuleForEach(x => x.Items).SetValidator(new OrderItemValidator());

        RuleFor(x => x.ShippingAddress)
            .NotEmpty().WithMessage("Shipping address is required")
            .Length(10, 500).WithMessage("Shipping address must be between 10 and 500 characters");

        RuleFor(x => x.TotalAmount)
            .Must(amount => amount > 0).WithMessage("Order total must be greater than zero");

        RuleForMoney(x => x.TotalAmount);
    }
}

public class OrderItemValidator : BaseValidator<OrderItem>
{
    public OrderItemValidator()
    {
        RuleFor(x => x.ProductId)
            .NotEmpty().WithMessage("Product ID is required");

        RuleFor(x => x.Quantity)
            .GreaterThan(0).WithMessage("Quantity must be greater than zero")
            .LessThanOrEqualTo(1000).WithMessage("Quantity cannot exceed 1000");

        RuleForMoney(x => x.UnitPrice);

        RuleFor(x => x.UnitPrice)
            .Must(money => money.Amount > 0).WithMessage("Unit price must be greater than zero");
    }
}

// Validation Behavior
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : class
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
    {
        _validators = validators;
    }

    public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
    {
        if (!_validators.Any())
        {
            return await next();
        }

        var context = new ValidationContext<TRequest>(request);

        var validationResults = await Task.WhenAll(
            _validators.Select(v => v.ValidateAsync(context, cancellationToken))
        );

        var failures = validationResults
            .Where(r => !r.IsValid)
            .SelectMany(r => r.Errors)
            .ToList();

        if (failures.Any())
        {
            var problemDetails = new ValidationProblemDetails
            {
                Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
                Title = "One or more validation errors occurred",
                Status = StatusCodes.Status400BadRequest,
                Detail = "Please refer to the errors property for additional details.",
                Instance = $"urn:validation:{Guid.NewGuid()}"
            };

            foreach (var error in failures)
            {
                if (problemDetails.Errors.ContainsKey(error.PropertyName))
                {
                    problemDetails.Errors[error.PropertyName] =
                        problemDetails.Errors[error.PropertyName].Concat(new[] { error.ErrorMessage }).ToArray();
                }
                else
                {
                    problemDetails.Errors.Add(error.PropertyName, new[] { error.ErrorMessage });
                }
            }

            throw new ValidationException(problemDetails);
        }

        return await next();
    }
}

// Global Exception Handler for Validation
public class ValidationExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ValidationExceptionMiddleware> _logger;

    public ValidationExceptionMiddleware(RequestDelegate next, ILogger<ValidationExceptionMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (ValidationException ex)
        {
            _logger.LogWarning("Validation failed: {@ValidationErrors}", ex.ProblemDetails.Errors);

            context.Response.StatusCode = ex.ProblemDetails.Status ?? StatusCodes.Status400BadRequest;
            context.Response.ContentType = "application/problem+json";

            var json = JsonSerializer.Serialize(ex.ProblemDetails, new JsonSerializerOptions
            {
                PropertyNamingPolicy = JsonNamingPolicy.CamelCase
            });

            await context.Response.WriteAsync(json);
        }
    }
}

📊 Structured Logging & Observability

Serilog Configuration
// Configuration
{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "System": "Warning"
      }
    },
    "WriteTo": [
      {
        "Name": "Console",
        "Args": {
          "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {CorrelationId} {TenantId} {Message:lj}{NewLine}{Exception}"
        }
      },
      {
        "Name": "File",
        "Args": {
          "path": "logs/app-.log",
          "rollingInterval": "Day",
          "retainedFileCountLimit": 30,
          "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {CorrelationId} {TenantId} {SourceContext} {Message:lj}{NewLine}{Exception}"
        }
      },
      {
        "Name": "Elasticsearch",
        "Args": {
          "nodeUris": "http://localhost:9200",
          "indexFormat": "marventa-logs-{0:yyyy.MM}",
          "autoRegisterTemplate": true
        }
      }
    ],
    "Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"],
    "Properties": {
      "Application": "Marventa.Framework"
    }
  }
}

// Service Registration
services.AddSerilog((serviceProvider, loggerConfiguration) =>
{
    loggerConfiguration
        .ReadFrom.Configuration(configuration)
        .Enrich.FromLogContext()
        .Enrich.WithProperty("Application", "Marventa.Framework")
        .Enrich.WithProperty("Environment", environment.EnvironmentName);
});
OpenTelemetry Integration
// Activity Service with Tenant Context
public class ActivityService : IActivityService
{
    private static readonly ActivitySource ActivitySource = new("Marventa.Framework");
    private readonly ITenantContext _tenantContext;
    private readonly ICorrelationContext _correlationContext;

    public Activity? StartActivity(string name, Dictionary<string, string>? tags = null)
    {
        var activity = ActivitySource.StartActivity(name);

        if (activity != null)
        {
            // Add tenant information
            if (_tenantContext.HasTenant)
            {
                activity.SetTag("tenant.id", _tenantContext.TenantId);
                activity.SetTag("tenant.name", _tenantContext.CurrentTenant?.Name);
            }

            // Add correlation context
            activity.SetTag("correlation.id", _correlationContext.CorrelationId);
            activity.SetTag("user.id", _correlationContext.UserId);

            // Add custom tags
            if (tags != null)
            {
                foreach (var tag in tags)
                {
                    activity.SetTag(tag.Key, tag.Value);
                }
            }
        }

        return activity;
    }

    public void RecordException(Activity activity, Exception exception)
    {
        activity?.SetStatus(ActivityStatusCode.Error, exception.Message);
        activity?.SetTag("error.type", exception.GetType().Name);
        activity?.SetTag("error.message", exception.Message);
        activity?.SetTag("error.stack", exception.StackTrace);
    }
}

// Structured Logging in Services
public class OrderService
{
    private readonly ILogger<OrderService> _logger;
    private readonly ITenantContext _tenantContext;
    private readonly ICorrelationContext _correlationContext;

    public async Task<Order> CreateOrderAsync(CreateOrderCommand command)
    {
        using var activity = _activityService.StartActivity("order.create");

        // Structured logging with context
        using var scope = _logger.BeginScope(new Dictionary<string, object>
        {
            ["TenantId"] = _tenantContext.TenantId ?? "global",
            ["CorrelationId"] = _correlationContext.CorrelationId,
            ["UserId"] = _correlationContext.UserId ?? "anonymous",
            ["OperationType"] = "CreateOrder"
        });

        try
        {
            _logger.LogInformation("Creating order for customer {CustomerId} with {ItemCount} items",
                command.CustomerId, command.Items.Count);

            var order = new Order(command.CustomerId, command.Items);

            _logger.LogInformation("Order {OrderId} created successfully with total {Total}",
                order.Id, order.Total.Format());

            activity?.SetTag("order.id", order.Id.ToString());
            activity?.SetStatus(ActivityStatusCode.Ok);

            return order;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to create order for customer {CustomerId}", command.CustomerId);
            _activityService.RecordException(activity!, ex);
            throw;
        }
    }
}

// OpenTelemetry Configuration
services.AddOpenTelemetry()
    .WithTracing(builder =>
    {
        builder
            .AddSource("Marventa.Framework")
            .AddAspNetCoreInstrumentation(options =>
            {
                options.RecordException = true;
                options.EnrichWithHttpRequest = (activity, request) =>
                {
                    activity.SetTag("http.request.size", request.ContentLength);
                    activity.SetTag("http.client.ip", request.HttpContext.Connection.RemoteIpAddress?.ToString());
                };
                options.EnrichWithHttpResponse = (activity, response) =>
                {
                    activity.SetTag("http.response.size", response.ContentLength);
                };
            })
            .AddHttpClientInstrumentation()
            .AddEntityFrameworkCoreInstrumentation()
            .AddRedisInstrumentation()
            .AddJaegerExporter()
            .AddConsoleExporter();
    })
    .WithMetrics(builder =>
    {
        builder
            .AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation()
            .AddRuntimeInstrumentation()
            .AddPrometheusExporter();
    });

🔍 Search with Elasticsearch

// Configuration
{
  "Elasticsearch": {
    "Uri": "http://localhost:9200",
    "DefaultIndex": "marventa-search",
    "Username": "elastic",
    "Password": "changeme",
    "EnableDebugMode": false
  }
}

// Search Service Implementation
public class ElasticsearchService : ISearchService
{
    private readonly HttpClient _httpClient;
    private readonly ElasticsearchOptions _options;
    private readonly ITenantContext _tenantContext;

    public async Task<SearchResult<T>> SearchAsync<T>(SearchRequest request, CancellationToken cancellationToken = default) where T : class
    {
        var indexName = GetTenantScopedIndex(request.IndexName ?? _options.DefaultIndex);

        var searchQuery = new
        {
            query = new
            {
                bool = new
                {
                    must = new[]
                    {
                        new { multi_match = new { query = request.Query, fields = new[] { "*" } } }
                    },
                    filter = new[]
                    {
                        new { term = new { tenant_id = _tenantContext.TenantId ?? "global" } }
                    }
                }
            },
            from = (request.Page - 1) * request.PageSize,
            size = request.PageSize,
            highlight = new
            {
                fields = new { @"*" = new { } }
            }
        };

        var response = await _httpClient.PostAsync($"{indexName}/_search",
            new StringContent(JsonSerializer.Serialize(searchQuery), Encoding.UTF8, "application/json"),
            cancellationToken);

        var content = await response.Content.ReadAsStringAsync(cancellationToken);
        var elasticResponse = JsonSerializer.Deserialize<ElasticsearchResponse<T>>(content);

        return new SearchResult<T>
        {
            Items = elasticResponse.Hits.Hits.Select(h => h.Source).ToList(),
            TotalCount = elasticResponse.Hits.Total.Value,
            Page = request.Page,
            PageSize = request.PageSize,
            ExecutionTime = elasticResponse.Took
        };
    }

    public async Task IndexDocumentAsync<T>(T document, string? id = null, string? indexName = null, CancellationToken cancellationToken = default) where T : class
    {
        indexName = GetTenantScopedIndex(indexName ?? _options.DefaultIndex);
        id ??= Guid.NewGuid().ToString();

        // Add tenant context to document
        var documentWithContext = new
        {
            tenant_id = _tenantContext.TenantId ?? "global",
            indexed_at = DateTime.UtcNow,
            document = document
        };

        var response = await _httpClient.PutAsync($"{indexName}/_doc/{id}",
            new StringContent(JsonSerializer.Serialize(documentWithContext), Encoding.UTF8, "application/json"),
            cancellationToken);

        response.EnsureSuccessStatusCode();
    }

    private string GetTenantScopedIndex(string baseIndex)
    {
        var tenantId = _tenantContext.TenantId ?? "global";
        return $"{baseIndex}-{tenantId}";
    }
}

// Usage Example
public class ProductSearchService
{
    private readonly ISearchService _searchService;

    public async Task<SearchResult<ProductSearchDto>> SearchProductsAsync(string query, int page = 1, int pageSize = 20)
    {
        var searchRequest = new SearchRequest
        {
            Query = query,
            Page = page,
            PageSize = pageSize,
            IndexName = "products"
        };

        return await _searchService.SearchAsync<ProductSearchDto>(searchRequest);
    }

    public async Task IndexProductAsync(Product product)
    {
        var searchDto = new ProductSearchDto
        {
            Id = product.Id,
            Name = product.Name,
            Description = product.Description,
            Price = product.Price.Amount,
            Currency = product.Price.Currency.Code,
            CategoryName = product.Category?.Name,
            Tags = product.Tags,
            CreatedAt = product.CreatedDate
        };

        await _searchService.IndexDocumentAsync(searchDto, product.Id.ToString(), "products");
    }
}

☁️ Cloud Storage Abstraction

// Storage Service Usage
public class DocumentService
{
    private readonly IStorageService _storage;

    public async Task<StorageFile> UploadDocumentAsync(IFormFile file, string folder = "documents")
    {
        using var stream = file.OpenReadStream();
        return await _storage.UploadAsync(stream, file.FileName, folder);
    }

    public async Task<byte[]> DownloadDocumentAsync(string fileKey)
    {
        return await _storage.DownloadBytesAsync(fileKey);
    }

    public async Task<string> GeneratePreSignedUrlAsync(string fileKey, TimeSpan expiration)
    {
        return await _storage.GetPresignedUrlAsync(fileKey, expiration);
    }
}

// Configuration for Different Providers
public class StorageConfiguration
{
    // AWS S3
    services.Configure<CloudStorageOptions>(options =>
    {
        options.Provider = StorageProvider.S3;
        options.ConnectionString = "DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=mykey";
        options.ContainerName = "documents";
        options.BaseUrl = "https://mybucket.s3.amazonaws.com";
    });

    // Azure Blob Storage
    services.Configure<CloudStorageOptions>(options =>
    {
        options.Provider = StorageProvider.AzureBlob;
        options.ConnectionString = "DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=mykey";
        options.ContainerName = "documents";
    });

    // Google Cloud Storage
    services.Configure<CloudStorageOptions>(options =>
    {
        options.Provider = StorageProvider.GoogleCloud;
        options.ConnectionString = "/path/to/service-account.json";
        options.ContainerName = "my-bucket";
    });
}

// Multi-tenant Storage with Tenant Isolation
public class TenantStorageService
{
    private readonly IStorageService _storage;
    private readonly ITenantContext _tenantContext;

    public async Task<StorageFile> UploadTenantFileAsync(IFormFile file, string category)
    {
        var tenantFolder = $"tenant_{_tenantContext.TenantId}/{category}";
        using var stream = file.OpenReadStream();

        return await _storage.UploadAsync(stream, file.FileName, tenantFolder);
    }

    public async Task<IEnumerable<StorageFile>> ListTenantFilesAsync(string category)
    {
        var tenantFolder = $"tenant_{_tenantContext.TenantId}/{category}";
        return await _storage.ListFilesAsync(tenantFolder);
    }
}

🌱 Database Seed Data

// Program.cs - Seed database on startup
public class Program
{
    public static async Task Main(string[] args)
    {
        var app = CreateApp(args);

        // Seed database with initial data
        await app.SeedDatabaseAsync<ApplicationDbContext>();

        await app.RunAsync();
    }

    private static WebApplication CreateApp(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        // Add Marventa services
        builder.Services.AddMarventaV13(builder.Configuration, "MyEcommerce", "1.0.0");

        // Add database seeding
        builder.Services.AddDatabaseSeeding();

        var app = builder.Build();
        app.UseMarventaMiddleware();

        return app;
    }
}

// Custom seeder for specific entities
public class CustomDatabaseSeeder : IDatabaseSeeder
{
    private readonly ILogger<CustomDatabaseSeeder> _logger;

    public async Task SeedAsync<TContext>(TContext context) where TContext : DbContext
    {
        _logger.LogInformation("Seeding custom data");

        // Seed categories
        var categorySet = context.Set<Category>();
        if (!await categorySet.AnyAsync())
        {
            var categories = new[]
            {
                new Category { Name = "Electronics", Description = "Electronic devices" },
                new Category { Name = "Clothing", Description = "Fashion items" }
            };

            categorySet.AddRange(categories);
            await context.SaveChangesAsync();
        }

        // Seed products
        var productSet = context.Set<Product>();
        if (!await productSet.AnyAsync())
        {
            var electronics = await categorySet.FirstAsync(c => c.Name == "Electronics");

            var products = new[]
            {
                new Product
                {
                    Name = "Smartphone",
                    Description = "Latest smartphone",
                    Price = new Money(699.99m, Currency.USD),
                    CategoryId = electronics.Id,
                    Stock = 50
                }
            };

            productSet.AddRange(products);
            await context.SaveChangesAsync();
        }
    }
}

// Multi-tenant seeding with tenant-specific data
public class TenantAwareSeeder
{
    private readonly ITenantContext _tenantContext;

    public async Task SeedTenantDataAsync(string tenantId)
    {
        // Set tenant context
        _tenantContext.SetTenant(tenantId);

        // Tenant-specific seeding
        var tenantProducts = GetTenantSpecificProducts(tenantId);
        // ... seed logic
    }

    private Product[] GetTenantSpecificProducts(string tenantId)
    {
        return tenantId switch
        {
            "demo" => new[] { new Product { Name = "Demo Product" } },
            "enterprise" => new[] { new Product { Name = "Enterprise Product" } },
            _ => Array.Empty<Product>()
        };
    }
}

// Configuration for seed data
{
  "SeedData": {
    "Enabled": true,
    "DefaultTenants": [
      {
        "Id": "demo",
        "Name": "Demo Company"
      }
    ]
  }
}

Architecture

The framework follows Clean Architecture principles:

  • Core - Domain entities, interfaces, and shared abstractions
  • Domain - Business logic, aggregates, and domain events
  • Application - CQRS handlers, validators, and application services
  • Infrastructure - External service implementations (database, messaging, caching)
  • Web - Controllers, middleware, and API concerns

Contributing

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/my-feature
  3. Commit your changes: git commit -am 'Add my feature'
  4. Push to the branch: git push origin feature/my-feature
  5. Submit a pull request

License

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

Support


Built with for .NET developers by Adem Kınataş

Product Compatible and additional computed target framework versions.
.NET 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 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
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 217 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 210 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 246 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.

v2.0.5: Comprehensive README documentation update with reorganized features by usage priority. Added structured logging with Serilog, detailed caching (Redis/In-Memory), messaging (RabbitMQ/Kafka), and Elasticsearch search examples. Professional documentation with complete usage examples for all 25+ enterprise features.