OElite.Common.Hosting.AspNetCore 5.0.9-develop.510
OElite.Common.Hosting.AspNetCore
ASP.NET Core web application hosting and lifecycle management for OElite applications.
Overview
OElite.Common.Hosting.AspNetCore extends the generic OElite.Common.Hosting package with comprehensive ASP.NET Core web application features. It provides standardized patterns for web APIs, authentication, CORS, Swagger documentation, middleware pipeline, and model transformation for HTTP responses.
This package is required for all OElite web applications and provides the recommended OeApp.RunWebAppAsync() and OeApp.RunHybridAppAsync() patterns referenced throughout the OElite coding standards.
Features
🚀 Streamlined Application Lifecycle
- OeApp.RunWebAppAsync: Complete web application setup with one method call
- OeApp.RunHybridAppAsync: Console applications with web API endpoints
- Automatic Configuration: Service registration, middleware pipeline, authentication
- Graceful Shutdown: Proper resource cleanup and service disposal
🔐 Comprehensive Authentication
- JWT Bearer Authentication: Automatic JWT validation and claims processing
- OElite Claim Integration: Built-in support for merchant, user, and client claims
- Multi-tenant Security: Request context extraction from JWT tokens
- Flexible Configuration: Enable/disable authentication per application
🌐 Advanced CORS Management
- Smart CORS Policies: Automatic origin detection and validation
- Environment-aware: Different policies for development and production
- Flexible Configuration: Custom CORS options with sensible defaults
📚 Integrated Swagger Documentation
- Model Transformation Aware: Documents transformed response models
- JWT Authentication: Built-in Swagger authentication configuration
- Automatic Generation: API documentation with minimal configuration
- Custom Attributes: Enhanced documentation with transformation annotations
🔄 HTTP Model Transformation
- Response Transformation: Automatic model transformation for HTTP responses
- Context-aware: Different transformations for different request contexts
- Performance Optimized: Efficient transformation pipeline
- Swagger Integration: Documented transformed response types
🛡️ Production-ready Middleware
- Global Exception Handling: Centralized error handling and logging
- Request Logging: Comprehensive request/response logging with Serilog
- Rate Limiting: Built-in rate limiting with OElite.Restme.RateLimiting
- Input Validation: Enhanced API input formatters and validation
Quick Start
1. Installation
dotnet add package OElite.Common.Hosting.AspNetCore
2. Simple Web API Application
using OElite.Common.Hosting.AspNetCore.Extensions;
using OElite.Common.Infrastructure;
public class MyApiConfig : BaseAppConfig
{
public override string? DbCentreFullClassName =>
"MyApi.Data.ApiDbCentre, MyApi.Data";
public MyApiConfig(string jsonConfig, ILogger? logger = null)
: base(OeAppType.GeneralWebApi, jsonConfig, logger)
{
}
}
public class Program
{
public static async Task Main(string[] args)
{
await OeApp.RunWebAppAsync<MyApiConfig>(
args: args,
applicationName: "MyApi",
enableAuthentication: true,
corsOptions: cors =>
{
cors.AllowedOrigins.Add("https://myapp.com");
cors.AllowCredentials = true;
});
}
}
3. Custom Service and App Configuration
public class Program
{
public static async Task Main(string[] args)
{
await OeApp.RunWebAppAsync<MyApiConfig>(
args: args,
applicationName: "MyAdvancedApi",
configureServices: builder =>
{
// Add custom services
builder.Services.AddScoped<ICustomService, CustomService>();
// Add custom options
builder.Services.Configure<MyOptions>(
builder.Configuration.GetSection("MyOptions"));
},
configureApp: async app =>
{
// Add custom middleware or endpoints
app.UseRouting();
app.MapControllers();
// Custom health check endpoint
app.MapGet("/custom-health", () => Results.Ok(new { Status = "Healthy" }));
},
dependencyInjectionOptions: options =>
{
// Customize service discovery
options.IncludeAssemblyPrefixes.Add("MyCustom");
options.ExcludeNamespacePrefixes.Add("MyCustom.Legacy");
});
}
}
4. Hybrid Console + API Application
public class Program
{
public static async Task Main(string[] args)
{
await OeApp.RunHybridAppAsync<MyProcessorConfig>(
args: args,
applicationName: "DataProcessor",
backgroundWorkAction: async (serviceProvider, cancellationToken) =>
{
// Background processing logic
var processor = serviceProvider.GetRequiredService<IDataProcessor>();
while (!cancellationToken.IsCancellationRequested)
{
await processor.ProcessDataAsync();
await Task.Delay(TimeSpan.FromMinutes(5), cancellationToken);
}
},
enableWebApi: true, // Exposes management API
enableAuthentication: false); // Management API without auth
}
}
Advanced Features
Authentication Configuration
JWT Bearer Setup
// Automatic JWT configuration based on app config
public class MyApiConfig : BaseAppConfig
{
public MyApiConfig(string jsonConfig) : base(OeAppType.GeneralWebApi, jsonConfig)
{
// JWT settings automatically loaded from:
// "oelite:authentication:jwtSecret"
// "oelite:authentication:issuer"
// "oelite:authentication:audience"
}
}
// JWT configuration in appsettings.json
{
"oelite": {
"authentication": {
"jwtSecret": "your-secret-key-here",
"issuer": "oelite-platform",
"audience": "oelite-platform",
"accessTokenExpirationMinutes": 30
}
}
}
Using Authentication in Controllers
[ApiController]
[Route("v{version:apiVersion}/[controller]")]
[Authorize] // Requires JWT authentication
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
public ProductsController(IProductService productService)
{
_productService = productService;
}
[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetProduct(string id)
{
// RequestContext is automatically populated from JWT claims
var requestContext = HttpContext.GetOEliteRequestContext();
var product = await _productService.GetProductAsync(id, requestContext);
return Ok(product);
}
[HttpPost]
[RequiredRoles("admin", "manager")] // Custom authorization
public async Task<ActionResult<Product>> CreateProduct([FromBody] CreateProductRequest request)
{
var requestContext = HttpContext.GetOEliteRequestContext();
var product = await _productService.CreateProductAsync(request, requestContext);
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
}
}
Request Context Extraction
public static class HttpContextExtensions
{
public static RequestContext GetOEliteRequestContext(this HttpContext context)
{
var user = context.User;
return new RequestContext
{
MerchantId = user.GetMerchantId(),
UserId = user.GetUserId(),
ContactId = user.GetContactId(),
ClientId = user.GetClientId(),
ClientIp = context.Connection.RemoteIpAddress,
UserAgent = context.Request.Headers.UserAgent,
CorrelationId = context.TraceIdentifier
};
}
}
// Extension methods for claims extraction
public static class ClaimsPrincipalExtensions
{
public static DbObjectId? GetMerchantId(this ClaimsPrincipal user)
{
var value = user.FindFirst(OEliteClaimTypes.MerchantId)?.Value;
return DbObjectId.TryParse(value, out var id) ? id : null;
}
public static DbObjectId? GetUserId(this ClaimsPrincipal user)
{
var value = user.FindFirst(OEliteClaimTypes.UserId)?.Value;
return DbObjectId.TryParse(value, out var id) ? id : null;
}
}
CORS Configuration
Advanced CORS Setup
await OeApp.RunWebAppAsync<MyApiConfig>(
args: args,
applicationName: "MyApi",
corsOptions: cors =>
{
// Allow specific origins
cors.AllowedOrigins.Add("https://app.mycompany.com");
cors.AllowedOrigins.Add("https://admin.mycompany.com");
// Development origins
if (Environment.IsDevelopment())
{
cors.AllowedOrigins.Add("http://localhost:3000");
cors.AllowedOrigins.Add("http://localhost:3001");
}
// Credentials and headers
cors.AllowCredentials = true;
cors.AllowedHeaders.Add("X-Custom-Header");
cors.ExposedHeaders.Add("X-Response-Time");
// Preflight cache
cors.PreflightMaxAge = TimeSpan.FromHours(1);
});
Environment-specific CORS
public class ApiCorsOptions : OEliteCorsOptions
{
public ApiCorsOptions()
{
if (Environment.IsDevelopment())
{
// Development: Allow localhost origins
AllowedOrigins.AddRange(new[]
{
"http://localhost:3000",
"http://localhost:3001",
"https://localhost:3000",
"https://localhost:3001"
});
AllowCredentials = true;
}
else if (Environment.IsStaging())
{
// Staging: Allow staging domains
AllowedOrigins.Add("https://staging.myapp.com");
AllowCredentials = true;
}
else
{
// Production: Strict CORS policy
AllowedOrigins.Add("https://myapp.com");
AllowedOrigins.Add("https://app.myapp.com");
AllowCredentials = true;
}
// Common headers for all environments
AllowedHeaders.AddRange(new[] { "Authorization", "Content-Type", "X-Requested-With" });
ExposedHeaders.Add("X-Pagination-Count");
}
}
Model Transformation for HTTP
Transformation-aware Controllers
[ApiController]
[Route("v{version:apiVersion}/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
private readonly IModelTransformationService _transformationService;
public ProductsController(
IProductService productService,
IModelTransformationService transformationService)
{
_productService = productService;
_transformationService = transformationService;
}
[HttpGet("{id}")]
[TransformedResponse<ProductApiModel>] // Swagger documentation
public async Task<ActionResult> GetProduct(string id, [FromQuery] string format = "api")
{
var product = await _productService.GetProductAsync(id);
// Transform based on request format
var context = new TransformationContext
{
TargetFormat = format,
Properties = new Dictionary<string, object>
{
["IncludeInventory"] = HttpContext.Request.Query.ContainsKey("includeInventory"),
["UserId"] = HttpContext.GetOEliteRequestContext().UserId
}
};
var transformed = await _transformationService.TransformAsync(product, context);
return Ok(transformed);
}
[HttpGet]
[TransformedResponse<List<ProductListModel>>]
public async Task<ActionResult> GetProducts(
[FromQuery] int page = 1,
[FromQuery] int size = 20,
[FromQuery] string format = "list")
{
var products = await _productService.GetProductsAsync(page, size);
var context = new TransformationContext { TargetFormat = format };
var transformed = await _transformationService.TransformCollectionAsync(products, context);
return Ok(new
{
Data = transformed,
Pagination = new { Page = page, Size = size, Total = products.Count }
});
}
}
Swagger Integration with Transformations
[HttpGet("{id}")]
[SwaggerOperation(
Summary = "Get product by ID",
Description = "Returns a product with optional transformation")]
[SwaggerResponse(200, "Product retrieved successfully", typeof(ProductApiModel))]
[SwaggerResponse(404, "Product not found")]
[TransformedResponse<ProductApiModel>]
public async Task<ActionResult> GetProduct(
[SwaggerParameter("Product ID", Required = true)] string id,
[SwaggerParameter("Response format (api, mobile, admin)")] string format = "api")
{
// Implementation...
}
Custom Middleware Integration
Global Exception Handling
// Custom middleware is automatically registered by OeApp.RunWebAppAsync
public class CustomApiExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<CustomApiExceptionMiddleware> _logger;
public CustomApiExceptionMiddleware(RequestDelegate next, ILogger<CustomApiExceptionMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (ValidationException ex)
{
await HandleValidationExceptionAsync(context, ex);
}
catch (BusinessException ex)
{
await HandleBusinessExceptionAsync(context, ex);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception occurred");
await HandleGenericExceptionAsync(context, ex);
}
}
private async Task HandleValidationExceptionAsync(HttpContext context, ValidationException ex)
{
context.Response.StatusCode = 400;
var response = new
{
Error = "Validation failed",
Details = ex.Errors.Select(e => new { Field = e.PropertyName, Message = e.ErrorMessage })
};
await context.Response.WriteAsync(JsonSerializer.Serialize(response));
}
}
// Register custom middleware
await OeApp.RunWebAppAsync<MyApiConfig>(
args: args,
applicationName: "MyApi",
configureApp: async app =>
{
// Custom middleware is added after OElite middleware
app.UseMiddleware<CustomApiExceptionMiddleware>();
});
Request/Response Logging
// Built-in request logging middleware is automatically configured
// Configure additional logging in appsettings.json
{
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"OElite.Common.Hosting.AspNetCore": "Debug"
}
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "logs/api-.txt",
"rollingInterval": "Day",
"retainedFileCountLimit": 30,
"outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}"
}
}
]
}
}
Swagger Documentation
Automatic Swagger Setup
// Swagger is automatically configured with JWT authentication
// Access at: https://yourapi.com/swagger
// The following features are included automatically:
// - JWT Bearer authentication UI
// - Model transformation documentation
// - API versioning support
// - Response type documentation
// - Custom operation filters
Custom Swagger Configuration
await OeApp.RunWebAppAsync<MyApiConfig>(
args: args,
applicationName: "MyApi",
configureServices: builder =>
{
builder.Services.ConfigureSwaggerGen(options =>
{
// Custom API information
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "My Custom API",
Version = "v1.0",
Description = "Advanced API with OElite transformation",
Contact = new OpenApiContact
{
Name = "API Support",
Email = "support@mycompany.com"
}
});
// Include XML comments
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
options.IncludeXmlComments(xmlPath);
// Custom operation filters
options.OperationFilter<CustomOperationFilter>();
});
});
Performance Considerations
Response Transformation Performance
- Model transformations are cached per request context
- Use
TransformationContext.Propertiesfor request-specific data - Avoid heavy computations in transformation
CanHandlemethods
Authentication Performance
- JWT tokens are validated once per request and cached
- Claims extraction is optimized for OElite claim types
- Request context creation is lazy and cached
Middleware Pipeline Optimization
- Critical middleware (authentication, CORS) runs first
- Exception handling middleware runs early to catch all errors
- Request logging middleware has minimal performance impact
Memory Management
- Services are registered as singletons where appropriate
- HTTP clients are properly managed through IHttpClientFactory
- Model transformation services use object pooling for performance
Application Patterns
API-only Application
public class Program
{
public static async Task Main(string[] args)
{
await OeApp.RunWebAppAsync<ApiConfig>(
args: args,
applicationName: "ProductApi",
enableAuthentication: true,
configureApp: async app =>
{
// API-only configuration
app.UseRouting();
app.MapControllers();
// Health checks
app.MapGet("/health", () => Results.Ok(new { Status = "Healthy", Timestamp = DateTime.UtcNow }));
});
}
}
Full-stack Web Application
public class Program
{
public static async Task Main(string[] args)
{
await OeApp.RunWebAppAsync<WebAppConfig>(
args: args,
applicationName: "ECommerceApp",
enableAuthentication: true,
configureServices: builder =>
{
// Add MVC with views
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
},
configureApp: async app =>
{
// Static files
app.UseStaticFiles();
// Routing
app.UseRouting();
// MVC routes
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
// API routes
app.MapControllers();
// Razor pages
app.MapRazorPages();
});
}
}
Background Service with Management API
public class Program
{
public static async Task Main(string[] args)
{
await OeApp.RunHybridAppAsync<ProcessorConfig>(
args: args,
applicationName: "DataProcessor",
backgroundWorkAction: async (serviceProvider, cancellationToken) =>
{
var processor = serviceProvider.GetRequiredService<IDataProcessor>();
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Starting background data processing...");
while (!cancellationToken.IsCancellationRequested)
{
try
{
await processor.ProcessBatchAsync();
await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken);
}
catch (Exception ex)
{
logger.LogError(ex, "Error in background processing");
await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken);
}
}
},
enableWebApi: true,
enableAuthentication: false, // Management API without auth
configureApp: async app =>
{
// Management endpoints
app.MapGet("/status", (IDataProcessor processor) =>
Results.Ok(processor.GetStatus()));
app.MapPost("/process-now", async (IDataProcessor processor) =>
{
await processor.ProcessBatchAsync();
return Results.Ok(new { Message = "Processing triggered" });
});
});
}
}
Testing Integration
Unit Testing with OElite.Common.Hosting.AspNetCore
public class ApiControllerTests
{
[Test]
public async Task TestProductEndpoint()
{
var services = new ServiceCollection();
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
["oelite:data:mongodb:app"] = "mongodb://localhost:27017/test",
["oelite:authentication:jwtSecret"] = "test-secret-key"
})
.Build();
var appConfig = new TestApiConfig(configuration);
// Configure services like OeApp does
services.AddOeApp(configuration, appConfig);
services.AddOEliteApi();
var provider = services.BuildServiceProvider();
var controller = new ProductsController(
provider.GetRequiredService<IProductService>(),
provider.GetRequiredService<IModelTransformationService>());
// Test controller methods
var result = await controller.GetProduct("test-id");
Assert.IsNotNull(result);
}
}
Integration Testing
public class ApiIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public ApiIntegrationTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Test]
public async Task Get_Products_Returns_Success()
{
var client = _factory.CreateClient();
// Add JWT token for authentication
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", GetTestJwtToken());
var response = await client.GetAsync("/v1.0/products");
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
var products = JsonSerializer.Deserialize<List<Product>>(content);
Assert.NotNull(products);
}
private string GetTestJwtToken()
{
// Generate test JWT token
var key = Encoding.ASCII.GetBytes("test-secret-key");
var tokenHandler = new JwtSecurityTokenHandler();
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(OEliteClaimTypes.UserId, "test-user-id"),
new Claim(OEliteClaimTypes.MerchantId, "test-merchant-id")
}),
Expires = DateTime.UtcNow.AddHours(1),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
}
Troubleshooting
Common Issues
Authentication Not Working
// Check JWT configuration in appsettings.json
{
"oelite": {
"authentication": {
"jwtSecret": "your-secret-key-here", // Must match token issuer
"issuer": "oelite-platform", // Must match token issuer
"audience": "oelite-platform" // Must match token audience
}
}
}
// Verify JWT token claims
public static void ValidateJwtToken(string token)
{
var handler = new JwtSecurityTokenHandler();
var jsonToken = handler.ReadJwtToken(token);
Console.WriteLine($"Issuer: {jsonToken.Issuer}");
Console.WriteLine($"Audience: {string.Join(", ", jsonToken.Audiences)}");
Console.WriteLine($"Claims: {string.Join(", ", jsonToken.Claims.Select(c => $"{c.Type}={c.Value}"))}");
}
CORS Issues
// Enable CORS debugging
await OeApp.RunWebAppAsync<MyApiConfig>(
args: args,
applicationName: "MyApi",
corsOptions: cors =>
{
cors.AllowedOrigins.Add("http://localhost:3000");
cors.AllowCredentials = true;
// Debug CORS in development
if (Environment.IsDevelopment())
{
cors.AllowAnyOrigin = true; // Only for development!
cors.AllowAnyHeader = true;
cors.AllowAnyMethod = true;
}
});
Swagger Not Accessible
// Ensure Swagger is enabled in production if needed
if (app.Environment.IsDevelopment() || app.Environment.IsStaging())
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
c.RoutePrefix = "swagger"; // Access at /swagger
});
}
Model Transformation Not Working
// Check transformer registration
var transformers = serviceProvider.GetServices<IModelTransformer>();
Console.WriteLine($"Registered transformers: {string.Join(", ", transformers.Select(t => t.GetType().Name))}");
// Debug transformation context
var context = new TransformationContext
{
TargetFormat = "api",
Properties = new Dictionary<string, object>()
};
var canHandle = transformer.CanHandle(context);
Console.WriteLine($"Transformer can handle context: {canHandle}");
Version History
- 5.0.9: Current version with .NET 10.0 support
- Enhanced JWT authentication with OElite claims
- Improved CORS handling with environment awareness
- Advanced Swagger integration with transformation documentation
- Optimized middleware pipeline for performance
- Added hybrid console+API application pattern
Related Packages
- OElite.Common: Core shared components and infrastructure
- OElite.Common.Hosting: Generic hosting foundation (required dependency)
- OElite.Services: Business logic services that integrate with authentication
- OElite.Restme.RateLimiting: Rate limiting integration for APIs
- Swashbuckle.AspNetCore: Swagger/OpenAPI documentation
License
Copyright © Phanes Technology Ltd. All rights reserved.
Support
For support and documentation, visit https://www.oelite.com
Integration with OElite Coding Standards
This package implements the patterns described in:
Always use OeApp.RunWebAppAsync() for web applications as specified in the coding standards.
No packages depend on OElite.Common.Hosting.AspNetCore.
.NET 10.0
- OElite.Common.Hosting (>= 5.0.9-develop.510)
- Swashbuckle.AspNetCore.Annotations (>= 6.5.0)
- Swashbuckle.AspNetCore (>= 7.2.0)
- Serilog.Sinks.File (>= 6.0.0)
- Serilog.Sinks.Console (>= 6.0.0)
- Serilog.Extensions.Hosting (>= 9.0.0)
- Serilog (>= 4.2.0)
- OElite.Restme.RateLimiting (>= 2.1.1-develop.508)
- OElite.Restme.Hosting (>= 2.1.1-develop.508)
- Newtonsoft.Json (>= 13.0.3)
- Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer (>= 5.1.0)
- Microsoft.AspNetCore.Mvc.Versioning (>= 5.1.0)
- Microsoft.AspNetCore.Mvc.Formatters.Json (>= 2.2.0)
- Microsoft.AspNetCore.Mvc.Core (>= 2.2.5)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 10.0.0)
- FluentValidation (>= 11.9.0)