![]() |
VOOZH | about |
dotnet add package Marventa.Framework --version 4.5.2
NuGet\Install-Package Marventa.Framework -Version 4.5.2
<PackageReference Include="Marventa.Framework" Version="4.5.2" />
<PackageVersion Include="Marventa.Framework" Version="4.5.2" />Directory.Packages.props
<PackageReference Include="Marventa.Framework" />Project file
paket add Marventa.Framework --version 4.5.2
#r "nuget: Marventa.Framework, 4.5.2"
#:package Marventa.Framework@4.5.2
#addin nuget:?package=Marventa.Framework&version=4.5.2Install as a Cake Addin
#tool nuget:?package=Marventa.Framework&version=4.5.2Install as a Cake Tool
Enterprise .NET Framework - Convention over Configuration
dotnet add package Marventa.Framework
var builder = WebApplication.CreateBuilder(args);
// ✨ ONE LINE - All services registered automatically
// Includes: Controllers, MediatR, FluentValidation, Mapster, CORS, and all configured features
builder.Services.AddMarventa(builder.Configuration);
var app = builder.Build();
// ✨ ONE LINE - All middleware configured automatically
// Includes: Exception handling, CORS, Authentication, Authorization, Rate Limiting, and Endpoints
app.UseMarventa(builder.Configuration);
app.Run();
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: 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");
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,
"RefreshTokenExpirationDays": 7,
"EnableTokenRotation": true,
"MaxRefreshTokensPerUser": 5,
"ValidateIpAddress": false
}
}
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: Securely manage refresh tokens with rotation and revocation support.
Generate and Use Refresh Token:
using Marventa.Framework.Security.Authentication.Abstractions;
public class AuthService
{
private readonly IJwtService _jwtService;
private readonly IRefreshTokenService _refreshTokenService;
public async Task<TokenResponse> LoginAsync(string email, string password)
{
// Validate user credentials...
var accessToken = _jwtService.GenerateAccessToken(user.Id.ToString(), user.Email);
var refreshToken = await _refreshTokenService.GenerateRefreshTokenAsync(
userId: user.Id.ToString(),
ipAddress: HttpContext.Connection.RemoteIpAddress?.ToString()
);
return new TokenResponse
{
AccessToken = accessToken,
RefreshToken = refreshToken.Token,
ExpiresAt = refreshToken.ExpiresAt
};
}
public async Task<TokenResponse> RefreshTokenAsync(string refreshToken, string ipAddress)
{
// Validate refresh token
var validToken = await _refreshTokenService.ValidateRefreshTokenAsync(refreshToken);
if (validToken == null || !validToken.IsActive)
{
throw new UnauthorizedException("Invalid or expired refresh token");
}
// Rotate refresh token (old token auto-revoked)
var newRefreshToken = await _refreshTokenService.RotateRefreshTokenAsync(
oldToken: refreshToken,
ipAddress: ipAddress
);
// Generate new access token
var accessToken = _jwtService.GenerateAccessToken(newRefreshToken.UserId, "user@example.com");
return new TokenResponse
{
AccessToken = accessToken,
RefreshToken = newRefreshToken.Token,
ExpiresAt = newRefreshToken.ExpiresAt
};
}
public async Task<bool> LogoutAsync(string refreshToken, string ipAddress)
{
return await _refreshTokenService.RevokeRefreshTokenAsync(
token: refreshToken,
ipAddress: ipAddress,
reason: "User logout"
);
}
public async Task<bool> LogoutAllDevicesAsync(string userId, string ipAddress)
{
var revokedCount = await _refreshTokenService.RevokeAllUserTokensAsync(
userId: userId,
ipAddress: ipAddress,
reason: "Logout from all devices"
);
return revokedCount > 0;
}
public async Task<IEnumerable<RefreshToken>> GetUserActiveSessionsAsync(string userId)
{
return await _refreshTokenService.GetUserActiveTokensAsync(userId);
}
}
Token Response Model:
public class TokenResponse
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public DateTime ExpiresAt { get; set; }
}
Purpose: Secure password hashing with BCrypt and strength validation.
using Marventa.Framework.Security.Encryption.Abstractions;
public class UserService
{
private readonly IPasswordService _passwordService;
// Hash password
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}");
}
var hashedPassword = _passwordService.HashPassword(password);
// Save user with hashed password...
}
// Verify password
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);
// Check if password hash needs rehashing (BCrypt cost updated)
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
);
}
}
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");
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,
"RefreshTokenExpirationDays": 7,
"EnableTokenRotation": true,
"MaxRefreshTokensPerUser": 5,
"ValidateIpAddress": false
},
"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! 🚀
Security Services Refactored:
IJwtService with comprehensive token management (replaced IJwtTokenGenerator)IPasswordService with strength validation and secure password generation (replaced IPasswordHasher)IRefreshTokenService with token rotation and revocation supportMemory Cache Configuration:
MemoryCache options (SizeLimit, CompactionPercentage, ExpirationScanFrequency)OutputCache support with flexible policiesModular Architecture:
Enhanced Caching:
MIT License - See for details.
For questions, please open an issue on GitHub Issues.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 net8.0 is compatible. net8.0-android net8.0-android was computed. net8.0-browser net8.0-browser was computed. net8.0-ios net8.0-ios was computed. net8.0-maccatalyst net8.0-maccatalyst was computed. net8.0-macos net8.0-macos was computed. net8.0-tvos net8.0-tvos was computed. net8.0-windows net8.0-windows was computed. net9.0 net9.0 is compatible. net9.0-android net9.0-android was computed. net9.0-browser net9.0-browser was computed. net9.0-ios net9.0-ios was computed. net9.0-maccatalyst net9.0-maccatalyst was computed. net9.0-macos net9.0-macos was computed. net9.0-tvos net9.0-tvos was computed. net9.0-windows net9.0-windows was computed. net10.0 net10.0 was computed. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
This package is not used by any NuGet packages.
This package is not used by any popular GitHub repositories.