⚠ Deprecated: Legacy
Enterprise-grade framework for microservices architecture with CQRS, Event Sourcing, and DDD support. Ultra-simplified with Convention over Configuration. v5.2.0: Added MongoDB Repository Pattern - IMongoRepository and MongoRepository base classes with full CRUD, aggregation, text search, geospatial queries, and bulk operations support. Added .NET 10.0 Preview support. Enhanced Audit Logging with domain events. v5.1.0: COMPLETE ENTERPRISE TOOLKIT - Notification Services, Localization, Template Engine, Report Generation, Payment Gateway, Job Scheduler, Audit Logging. v5.0.0: Security hardening, Transactional Outbox, Repository Specification, Idempotency, Circuit Breaker, API Key Auth. Production-ready for complex enterprise applications. Supports .NET 8.0 (LTS), .NET 9.0, and .NET 10.0 (Preview)
$ dotnet add package Marventa.FrameworkEnterprise .NET Framework - Convention over Configuration
dotnet add package Marventa.Framework
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();
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();
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();
| 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 |
Core Features (Production-ready patterns):
Extended Features (Optional enterprise services):
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:
appsettings.json// 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
);
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;
}
}
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));
}
}
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;
}
}
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;
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!
}
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);
}
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;"
}
}
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>();
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);
}
}
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();
}
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 IDGetAllAsync() - Get all documentsFindAsync(predicate) - Find documents by expressionAddAsync(entity) - Insert single documentAddRangeAsync(entities) - Insert multiple documentsUpdateAsync(entity) - Update documentDeleteAsync(id) - Delete by IDDeleteAsync(entity) - Delete documentCountAsync(predicate) - Count documentsExistsAsync(predicate) - Check if document existsGetPagedAsync(pageNumber, pageSize, predicate) - Paginated resultsBenefits:
Purpose: MediatR is auto-registered with validation/logging/performance behaviors active.
// 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);
}
}
// 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);
}
}
Purpose: Automatically validates all requests with FluentValidation. Auto-active! Just write validator classes.
Purpose: Logs all requests/responses. Auto-active!
Purpose: Warns about requests taking longer than 500ms. Auto-active!
Purpose: Framework supports three caching strategies: InMemory, Redis, and Hybrid (two-level 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}");
}
}
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);
}
}
Configuration (appsettings.json):
{
"Caching": { "Type": "Redis" },
"Redis": {
"ConnectionString": "localhost:6379",
"InstanceName": "MyApp:"
}
}
Usage: Same interface as InMemory (ICacheService). Framework automatically switches based on configuration.
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:
Usage: Same ICacheService interface - completely transparent!
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);
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>();
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}");
});
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 });
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");
Configuration:
{
"Azure": {
"Storage": {
"ConnectionString": "your-connection-string",
"ContainerName": "uploads"
}
}
}
Usage: Same as Local Storage.
Configuration:
{
"AWS": {
"AccessKey": "your-key",
"SecretKey": "your-secret",
"Region": "us-east-1",
"BucketName": "my-bucket"
}
}
Usage: Same as Local Storage.
Configuration:
{
"Elasticsearch": {
"Uri": "http://localhost:9200"
}
}
Usage:
// Index
await _elasticsearchService.IndexAsync("products", product);
// Search
var results = await _elasticsearchService.SearchAsync<Product>("products", "laptop");
Purpose: Send notifications via Email, SMS, and Push Notifications with multiple provider support.
Supported Providers:
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
}
}
Supported Providers:
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"
}
}
Supported Providers:
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:
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:
?culture=tr-TR.AspNetCore.CultureDynamic 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
});
}
}
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
});
}
}
public async Task<byte[]> GenerateExcelReport(List<Order> orders)
{
return await _excelGenerator.GenerateAsync(orders, new ReportOptions
{
SheetName = "Orders",
AutoFitColumns = true
});
}
public async Task<byte[]> GenerateCsvReport(List<Customer> customers)
{
return await _csvGenerator.GenerateAsync(customers);
}
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;
}
}
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
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; }
}
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");
Configuration:
{
"OpenTelemetry": {
"ServiceName": "MyApp",
"OtlpEndpoint": "http://localhost:4317"
}
}
Purpose: Automatically traces HTTP, Database, and External API calls.
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);
Purpose: Generate cryptographically secure refresh token strings.
Important: The framework only provides token generation. You must implement your own:
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;
}
Purpose: Secure password hashing with Argon2id (winner of Password Hashing Competition 2015) and strength validation.
Why Argon2id?
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.
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:
[Authorize]
[RequirePermission("products.write")]
public async Task<IActionResult> Create([FromBody] CreateProductCommand command)
{
var result = await _mediator.Send(command);
return Ok(result);
}
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
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
Configuration:
{
"HealthChecks": {
"Enabled": "true"
}
}
Purpose: Creates /health endpoint, automatically monitors Database/Redis/RabbitMQ.
Check:
curl http://localhost:5000/health
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.0Header - Header: X-API-Version: 1.0MediaType - Accept: application/json;v=1.0Usage 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)
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:
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);
}
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");
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:
OutboxMessages table in same transactionOutboxProcessor polls every 30 secondsBuild 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 filterAddInclude() / AddInclude(string) - Eager loadingAddOrderBy() / AddOrderByDescending() - SortingApplyPaging(skip, take) - PaginationPrevent 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:
Idempotency-Key headerAdd 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:
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:
X-API-KeyComprehensive 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:
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
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"
}
}
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! 🚀
📦 MongoDB Repository Pattern:
IMongoRepository<TEntity> with async CRUD operations🚀 .NET 10 Support:
📚 Enhanced Documentation:
🎉 Notification Services:
🌍 Localization (i18n):
📄 Template Engine:
📊 Reporting:
💳 Payment Gateway:
⏰ Job Scheduler:
📝 Audit Logging:
🔒 Critical Security Fixes:
🏗️ Repository & Database Enhancements:
GetPagedAsync() with eager loading support to prevent memory issuesISoftDeletable📦 New Enterprise Patterns:
🔐 Advanced Security Features:
🚀 Infrastructure Improvements:
RemoveByPrefixAsync() implementationMIT License - See LICENSE for details.
For questions, please open an issue on GitHub Issues.