Acontplus.Services
2.1.6
dotnet add package Acontplus.Services --version 2.1.6
NuGet\Install-Package Acontplus.Services -Version 2.1.6
<PackageReference Include="Acontplus.Services" Version="2.1.6" />
<PackageVersion Include="Acontplus.Services" Version="2.1.6" />
<PackageReference Include="Acontplus.Services" />
paket add Acontplus.Services --version 2.1.6
#r "nuget: Acontplus.Services, 2.1.6"
#:package Acontplus.Services@2.1.6
#addin nuget:?package=Acontplus.Services&version=2.1.6
#tool nuget:?package=Acontplus.Services&version=2.1.6
Acontplus.Services
A comprehensive .NET service library providing business-grade patterns, security, device detection, request management, and intelligent exception handling for ASP.NET Core applications. Built with modern .NET 10 features and best practices.
💡 Infrastructure Services: For caching, circuit breakers, resilience patterns, and HTTP client factory, use Acontplus.Infrastructure
🚀 Features
🏗️ Service Architecture Patterns
- Service Layer: Clean separation of concerns with dependency injection
- Lookup Service: Cached lookup/reference data management with flexible SQL mapping
- Action Filters: Reusable cross-cutting concerns (validation, logging, security)
- Authorization Policies: Fine-grained access control for multi-tenant scenarios
- Middleware Pipeline: Properly ordered middleware for security and context management
🛡️ Advanced Exception Handling NEW!
- Flexible Design: Works with or without catch blocks - your choice!
- Smart Exception Translation: Preserves custom error codes from business logic
- DomainException Support: Automatic handling of domain exceptions with proper HTTP status codes
- Consistent API Responses: Standardized error format with categories and severity
- Intelligent Logging: Context-aware logging with appropriate severity levels
- Distributed Tracing: Correlation IDs and trace IDs for request tracking
- Multi-tenancy Support: Tenant ID tracking across requests
📖 Complete Exception Handling Guide
🔒 Security & Compliance
- Security Headers: Comprehensive HTTP security header management
- Content Security Policy: CSP nonce generation and management
- Client Validation: Client-ID based access control
- Tenant Isolation: Multi-tenant security policies
- JWT Authentication: Enterprise-grade JWT token validation
📱 Device & Context Awareness
- Device Detection: Smart device type detection from headers and user agents
- Request Context: Correlation IDs, tenant isolation, and request tracking
- Device-Aware Policies: Mobile and tablet-aware authorization policies
📊 Observability
- Request Logging: Structured logging with performance metrics
- Health Checks: Comprehensive health monitoring for application services
- Application Insights: Optional integration for telemetry and monitoring
📦 Installation
Required Packages
# Application services (this package)
dotnet add package Acontplus.Services
# Infrastructure services (caching, resilience, etc.)
dotnet add package Acontplus.Infrastructure
NuGet Package Manager
Install-Package Acontplus.Services
Install-Package Acontplus.Infrastructure
PackageReference
<PackageReference Include="Acontplus.Services" Version="1.5.0" />
<PackageReference Include="Acontplus.Infrastructure" Version="1.0.0" />
🎯 Quick Start
1. Add to Your Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add application services (authentication, security, device detection, exception handling)
builder.Services.AddApplicationServices(builder.Configuration);
// Add infrastructure services (caching, resilience, HTTP clients)
builder.Services.AddInfrastructureServices(builder.Configuration);
var app = builder.Build();
// Use application middleware pipeline (includes exception handling)
app.UseApplicationMiddleware(builder.Environment);
app.MapControllers();
app.Run();
2. Exception Handling - No Catch Needed! NEW!
// Business Layer - Just throw, middleware handles everything
public async Task<Customer> GetCustomerAsync(int id)
{
var customer = await _repository.GetByIdAsync(id);
if (customer is null)
{
throw new GenericDomainException(
ErrorType.NotFound,
"CUSTOMER_NOT_FOUND",
"Customer not found");
}
return customer;
}
Automatic Response:
{
"success": false,
"code": "404",
"message": "Customer not found",
"errors": [{
"code": "CUSTOMER_NOT_FOUND",
"message": "Customer not found",
"category": "business",
"severity": "warning"
}],
"correlationId": "abc-123"
}
Or Use Result Pattern:
public async Task<Result<Customer, DomainError>> GetCustomerAsync(int id)
{
try
{
var customer = await _repository.GetByIdAsync(id);
return customer ?? DomainError.NotFound("CUSTOMER_NOT_FOUND", "Not found");
}
catch (SqlDomainException ex)
{
return ex.ToDomainError();
}
}
// Controller
[HttpGet("{id}")]
public Task<IActionResult> GetCustomer(int id)
{
return _service.GetCustomerAsync(id).ToActionResultAsync();
}
3. Basic Configuration
Add to your appsettings.json:
{
"RequestContext": {
"EnableSecurityHeaders": true,
"RequireClientId": false,
"Csp": {
"AllowedFrameSources": ["https://www.youtube-nocookie.com"],
"AllowedScriptSources": ["https://cdn.jsdelivr.net"],
"AllowedConnectSources": ["https://api.yourdomain.com"]
}
},
"ExceptionHandling": {
"IncludeDebugDetailsInResponse": false,
"IncludeRequestDetails": true,
"LogRequestBody": false
},
"Caching": {
"UseDistributedCache": false
}
}
4. Use in Your Controller
[ApiController]
[Route("api/[controller]")]
public class HelloController : ControllerBase
{
private readonly ICacheService _cache;
private readonly IRequestContextService _context;
public HelloController(ICacheService cache, IRequestContextService context)
{
_cache = cache;
_context = context;
}
[HttpGet]
public async Task<IActionResult> Get()
{
var message = await _cache.GetOrCreateAsync("hello",
() => Task.FromResult("Hello from Acontplus.Services!"),
TimeSpan.FromMinutes(5));
return Ok(new {
Message = message,
CorrelationId = _context.GetCorrelationId()
});
}
}
🎯 Usage Examples
🟢 Basic Usage - Simple Setup
Perfect for small applications or getting started quickly.
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add application and infrastructure services
builder.Services.AddApplicationServices(builder.Configuration);
builder.Services.AddInfrastructureServices(builder.Configuration);
// Add controllers
builder.Services.AddControllers();
var app = builder.Build();
// Complete middleware pipeline in one call
app.UseApplicationMiddleware(builder.Environment);
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Basic Controller Example
[ApiController]
[Route("api/[controller]")]
public class BasicController : ControllerBase
{
private readonly ICacheService _cache;
private readonly IRequestContextService _context;
public BasicController(ICacheService cache, IRequestContextService context)
{
_cache = cache;
_context = context;
}
[HttpGet("hello")]
public async Task<IActionResult> Hello()
{
var message = await _cache.GetOrCreateAsync(
"hello-message",
() => Task.FromResult("Hello from Acontplus.Services!"),
TimeSpan.FromMinutes(5)
);
return Ok(new {
Message = message,
CorrelationId = _context.GetCorrelationId()
});
}
}
🟡 Intermediate Usage - Granular Control
For applications that need fine-grained control over services and middleware.
// Program.cs with granular control
var builder = WebApplication.CreateBuilder(args);
// Add services individually for more control
builder.Services.AddApplicationServices(builder.Configuration);
builder.Services.AddCachingServices(builder.Configuration);
builder.Services.AddResilienceServices(builder.Configuration);
builder.Services.AddAuthorizationPolicies(new List<string> { "web-app", "mobile-app" });
// Add health checks
builder.Services.AddApplicationHealthChecks(builder.Configuration);
builder.Services.AddInfrastructureHealthChecks();
// Add controllers with custom filters
builder.Services.AddControllers(options =>
{
options.Filters.Add<SecurityHeaderActionFilter>();
options.Filters.Add<RequestLoggingActionFilter>();
options.Filters.Add<ValidationActionFilter>();
});
var app = builder.Build();
// Configure middleware pipeline manually
app.UseSecurityHeaders(builder.Environment);
app.UseMiddleware<CspNonceMiddleware>();
app.UseMiddleware<RateLimitingMiddleware>();
app.UseMiddleware<RequestContextMiddleware>();
app.UseAcontplusExceptionHandling();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks("/health");
app.Run();
Intermediate Controller with Device Detection
[ApiController]
[Route("api/[controller]")]
public class IntermediateController : ControllerBase
{
private readonly ICacheService _cache;
private readonly IDeviceDetectionService _deviceDetection;
private readonly ICircuitBreakerService _circuitBreaker;
public IntermediateController(
ICacheService cache,
IDeviceDetectionService deviceDetection,
ICircuitBreakerService circuitBreaker)
{
_cache = cache;
_deviceDetection = deviceDetection;
_circuitBreaker = circuitBreaker;
}
[HttpGet("content")]
public async Task<IActionResult> GetContent()
{
var deviceType = _deviceDetection.DetectDeviceType(HttpContext);
var cacheKey = $"content:{deviceType}";
var content = await _cache.GetOrCreateAsync(cacheKey, async () =>
{
// Simulate external API call with circuit breaker
return await _circuitBreaker.ExecuteAsync(async () =>
{
await Task.Delay(100); // Simulate API call
return deviceType switch
{
DeviceType.Mobile => "Mobile-optimized content",
DeviceType.Tablet => "Tablet-optimized content",
_ => "Desktop content"
};
}, "content-api");
}, TimeSpan.FromMinutes(10));
return Ok(new { Content = content, DeviceType = deviceType.ToString() });
}
[HttpGet("health")]
public IActionResult GetHealth()
{
var circuitBreakerStatus = _circuitBreaker.GetCircuitBreakerState("content-api");
var cacheStats = _cache.GetStatistics();
return Ok(new
{
CircuitBreaker = circuitBreakerStatus,
Cache = new
{
TotalEntries = cacheStats.TotalEntries,
HitRate = $"{cacheStats.HitRatePercentage:F1}%"
}
});
}
}
🔴 Enterprise Usage - Full Configuration
Complete setup for enterprise applications with all features enabled.
// Program.cs for enterprise applications
var builder = WebApplication.CreateBuilder(args);
// Configure logging
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddApplicationInsights();
// Add all Acontplus services
builder.Services.AddApplicationServices(builder.Configuration);
builder.Services.AddInfrastructureServices(builder.Configuration);
// Add authentication and authorization
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
};
});
// Add authorization policies
builder.Services.AddAuthorizationPolicies(new List<string>
{
"web-app", "mobile-app", "admin-portal", "api-client"
});
// Add API documentation
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Add controllers
builder.Services.AddControllers();
var app = builder.Build();
// Configure middleware pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseApplicationMiddleware(app.Environment);
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks("/health");
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready")
});
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("live")
});
app.Run();
⚙️ Configuration Examples
Complete Configuration
{
"RequestContext": {
"EnableSecurityHeaders": true,
"FrameOptionsDeny": true,
"ReferrerPolicy": "strict-origin-when-cross-origin",
"RequireClientId": true,
"AllowedClientIds": ["web-app", "mobile-app", "admin-portal"],
"Csp": {
"AllowedImageSources": ["https://cdn.example.com"],
"AllowedStyleSources": ["https://fonts.googleapis.com"],
"AllowedScriptSources": ["https://cdn.example.com"],
"AllowedConnectSources": ["https://api.example.com"]
}
},
"Caching": {
"UseDistributedCache": false,
"MemoryCacheSizeLimit": 104857600
},
"Resilience": {
"CircuitBreaker": {
"Enabled": true,
"ExceptionsAllowedBeforeBreaking": 5
},
"RetryPolicy": {
"Enabled": true,
"MaxRetries": 3
}
},
"JwtSettings": {
"Issuer": "https://auth.acontplus.com",
"Audience": "api.acontplus.com",
"SecurityKey": "your-super-secret-key-at-least-32-characters-long",
"ClockSkew": "5",
"RequireHttps": "true"
}
}
📚 Core Services Reference
What's in Acontplus.Services
✅ Application Services
IRequestContextService- Request context management and correlationISecurityHeaderService- HTTP security headers and CSP managementIDeviceDetectionService- Device type detection and capabilitiesILookupService- Cached lookup/reference data management (NEW!)
✅ Action Filters
ValidationActionFilter- Model validationRequestLoggingActionFilter- Request/response loggingSecurityHeaderActionFilter- Security header injection
✅ Authorization Policies
RequireClientIdPolicy- Client ID validationTenantIsolationPolicy- Multi-tenant isolationDeviceTypePolicy- Device-aware authorization
✅ Middleware
RequestContextMiddleware- Request context extractionCspNonceMiddleware- CSP nonce generationApiExceptionMiddleware- Global exception handling
What's in Acontplus.Infrastructure
Note: These services require
Acontplus.Infrastructurepackage
✅ Infrastructure Services (from Acontplus.Infrastructure)
ICacheService- Caching (in-memory and Redis)ICircuitBreakerService- Circuit breaker patternsRetryPolicyService- Retry policiesResilientHttpClientFactory- Resilient HTTP clients
✅ Middleware (from Acontplus.Infrastructure)
RateLimitingMiddleware- Rate limiting
✅ Health Checks (from Acontplus.Infrastructure)
CacheHealthCheck- Cache service healthCircuitBreakerHealthCheck- Circuit breaker health
🚀 Features Examples
Lookup Service (NEW!)
Manage cached lookup/reference data from database queries with automatic caching.
// 1. Register in Program.cs
builder.Services.AddLookupService();
// 2. Use in controller
public class LookupsController : ControllerBase
{
private readonly ILookupService _lookupService;
public LookupsController(ILookupService lookupService)
{
_lookupService = lookupService;
}
[HttpGet]
public async Task<IActionResult> GetLookups(
[FromQuery] string? module = null,
[FromQuery] string? context = null)
{
var filterRequest = new FilterRequest
{
Filters = new Dictionary<string, object>
{
["module"] = module ?? "default",
["context"] = context ?? "general"
}
};
var result = await _lookupService.GetLookupsAsync(
"YourSchema.GetLookups", // Stored procedure name
filterRequest);
return result.Match(
success => Ok(ApiResponse.Success(success)),
error => BadRequest(ApiResponse.Failure(error)));
}
[HttpPost("refresh")]
public async Task<IActionResult> RefreshLookups()
{
var result = await _lookupService.RefreshLookupsAsync(
"YourSchema.GetLookups",
new FilterRequest());
return result.Match(
success => Ok(ApiResponse.Success(success)),
error => BadRequest(ApiResponse.Failure(error)));
}
}
Features:
- ✅ Automatic caching (30-minute TTL)
- ✅ Works with SQL Server and PostgreSQL
- ✅ Flexible SQL query mapping (all nullable properties)
- ✅ Supports hierarchical data (ParentId)
- ✅ Grouped results by table name
- ✅ Cache refresh on demand
SQL Stored Procedure Example:
CREATE PROCEDURE [YourSchema].[GetLookups]
@Module NVARCHAR(100) = NULL,
@Context NVARCHAR(100) = NULL
AS
BEGIN
SELECT
'Countries' AS TableName,
Id, Code, [Name] AS [Value], DisplayOrder,
NULL AS ParentId, IsDefault, IsActive,
Description, NULL AS Metadata
FROM Countries
WHERE IsActive = 1
ORDER BY DisplayOrder;
END
Response Format:
{
"status": "Success",
"data": {
"countries": [
{
"id": 1,
"code": "US",
"value": "United States",
"displayOrder": 1,
"isDefault": true,
"isActive": true,
"description": "United States of America",
"metadata": null
}
]
}
}
📚 Lookup Service - Complete Guide
Overview
The LookupService is a reusable, cached service for managing lookup/reference data across all Acontplus APIs. It's located in the Acontplus.Services NuGet package and works seamlessly with both PostgreSQL and SQL Server.
Architecture
Package Structure
Acontplus.Services/
├── Services/
│ ├── Abstractions/
│ │ └── ILookupService.cs # Interface
│ ├── Implementations/
│ │ └── LookupService.cs # Implementation
│ └── README.md
├── Extensions/
│ └── ServiceExtensions.cs # DI registration
└── GlobalUsings.cs
Acontplus.Core/
└── Dtos/
└── Responses/
└── LookupItem.cs # Shared DTO
Acontplus.Infrastructure/
└── Caching/
├── ICacheService.cs # Cache abstraction
├── MemoryCacheService.cs # In-memory implementation
└── DistributedCacheService.cs # Redis implementation
Design Decisions
Location: Acontplus.Services Package
Rationale:
- ✅ Database Agnostic: Works with both PostgreSQL and SQL Server through
IUnitOfWorkabstraction - ✅ Reusable: Available to all APIs via NuGet package
- ✅ Proper Layer: Application-level service, not infrastructure or persistence specific
- ✅ Dependencies: Already has access to Core and Infrastructure packages
Data Flow
Controller
↓
ILookupService.GetLookupsAsync()
↓
Check Cache (ICacheService)
↓ (cache miss)
IUnitOfWork.AdoRepository.GetFilteredDataSetAsync()
↓
Stored Procedure Execution
↓
Map DataSet → Dictionary<string, IEnumerable<LookupItem>>
↓
Store in Cache
↓
Return Result<T, DomainError>
Quick Start
1. Register Services (Program.cs)
// For development/single server
builder.Services.AddMemoryCache();
builder.Services.AddMemoryCacheService();
// OR for production/multi-server
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
});
builder.Services.AddDistributedCacheService();
// Register persistence (choose your database)
builder.Services.AddSqlServerPersistence<YourDbContext>(connectionString);
// OR
builder.Services.AddPostgresPersistence<YourDbContext>(connectionString);
// Register lookup service
builder.Services.AddLookupService();
2. Create Stored Procedure
SQL Server Example:
CREATE PROCEDURE [YourSchema].[GetLookups]
@Module NVARCHAR(100) = NULL,
@Context NVARCHAR(100) = NULL
AS
BEGIN
SET NOCOUNT ON;
-- Return multiple lookup tables
SELECT
'OrderStatuses' AS TableName,
Id,
Code,
[Name] AS [Value],
DisplayOrder,
NULL AS ParentId,
CAST(0 AS BIT) AS IsDefault,
CAST(1 AS BIT) AS IsActive,
Description,
NULL AS Metadata
FROM YourSchema.OrderStatuses
WHERE IsActive = 1
UNION ALL
SELECT
'PaymentMethods' AS TableName,
Id,
Code,
[Name] AS [Value],
SortOrder AS DisplayOrder,
NULL AS ParentId,
IsDefault,
IsActive,
NULL AS Description,
JSON_QUERY((SELECT Icon, Color FOR JSON PATH, WITHOUT_ARRAY_WRAPPER)) AS Metadata
FROM YourSchema.PaymentMethods
WHERE IsActive = 1
ORDER BY TableName, DisplayOrder;
END
PostgreSQL Example:
CREATE OR REPLACE FUNCTION your_schema.get_lookups(
p_module VARCHAR DEFAULT NULL,
p_context VARCHAR DEFAULT NULL
)
RETURNS TABLE (
table_name VARCHAR,
id INTEGER,
code VARCHAR,
value VARCHAR,
display_order INTEGER,
parent_id INTEGER,
is_default BOOLEAN,
is_active BOOLEAN,
description TEXT,
metadata JSONB
) AS $$
BEGIN
RETURN QUERY
SELECT
'orderStatuses'::VARCHAR AS table_name,
os.id,
os.code,
os.value,
os.display_order,
NULL::INTEGER AS parent_id,
FALSE AS is_default,
TRUE AS is_active,
os.description,
NULL::JSONB AS metadata
FROM your_schema.order_statuses os
WHERE os.is_active = TRUE
UNION ALL
SELECT
'paymentMethods'::VARCHAR,
pm.id,
pm.code,
pm.name AS value,
pm.sort_order AS display_order,
NULL::INTEGER,
pm.is_default,
pm.is_active,
NULL::TEXT,
jsonb_build_object('icon', pm.icon, 'color', pm.color) AS metadata
FROM your_schema.payment_methods pm
WHERE pm.is_active = TRUE
ORDER BY table_name, display_order;
END;
$$ LANGUAGE plpgsql;
3. Use in Controller
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class LookupsController : ControllerBase
{
private readonly ILookupService _lookupService;
public LookupsController(ILookupService lookupService)
{
_lookupService = lookupService;
}
/// <summary>
/// Get all lookups with caching
/// </summary>
[HttpGet]
[ProducesResponseType(typeof(ApiResponse<IDictionary<string, IEnumerable<LookupItem>>>), 200)]
public async Task<IActionResult> GetLookups(
[FromQuery] string? module = null,
[FromQuery] string? context = null,
CancellationToken cancellationToken = default)
{
var filterRequest = new FilterRequest
{
Filters = new Dictionary<string, object>
{
["module"] = module ?? "default",
["context"] = context ?? "general"
}
};
var result = await _lookupService.GetLookupsAsync(
"YourSchema.GetLookups", // SQL Server
// OR "your_schema.get_lookups" for PostgreSQL
filterRequest,
cancellationToken);
return result.Match(
success => Ok(ApiResponse.Success(success)),
error => BadRequest(ApiResponse.Failure(error)));
}
/// <summary>
/// Refresh lookups cache
/// </summary>
[HttpPost("refresh")]
[ProducesResponseType(typeof(ApiResponse<IDictionary<string, IEnumerable<LookupItem>>>), 200)]
public async Task<IActionResult> RefreshLookups(
[FromQuery] string? module = null,
[FromQuery] string? context = null,
CancellationToken cancellationToken = default)
{
var filterRequest = new FilterRequest
{
Filters = new Dictionary<string, object>
{
["module"] = module ?? "default",
["context"] = context ?? "general"
}
};
var result = await _lookupService.RefreshLookupsAsync(
"YourSchema.GetLookups",
filterRequest,
cancellationToken);
return result.Match(
success => Ok(ApiResponse.Success(success)),
error => BadRequest(ApiResponse.Failure(error)));
}
}
LookupItem DTO
All DTO properties are nullable for maximum flexibility:
public record LookupItem
{
public int? Id { get; init; }
public string? Code { get; init; }
public string? Value { get; init; }
public int? DisplayOrder { get; init; }
public int? ParentId { get; init; }
public bool? IsDefault { get; init; }
public bool? IsActive { get; init; }
public string? Description { get; init; }
public string? Metadata { get; init; }
}
Required Columns
Your stored procedure MUST return these columns:
| Column | Type | Required | Description |
|---|---|---|---|
TableName |
string | ✅ Yes | Groups results (e.g., "Countries", "States") |
Id |
int? | No | Unique identifier |
Code |
string? | No | Short code (e.g., "US", "CA") |
Value |
string? | No | Display text |
DisplayOrder |
int? | No | Sort order |
ParentId |
int? | No | For hierarchical data |
IsDefault |
bool? | No | Default selection |
IsActive |
bool? | No | Active/inactive flag |
Description |
string? | No | Tooltip or help text |
Metadata |
string? | No | JSON string for custom data |
Response Format
{
"status": "Success",
"code": "200",
"data": {
"orderStatuses": [
{
"id": 1,
"code": "PENDING",
"value": "Pending",
"displayOrder": 1,
"parentId": null,
"isDefault": true,
"isActive": true,
"description": "Order is pending confirmation",
"metadata": null
},
{
"id": 2,
"code": "CONFIRMED",
"value": "Confirmed",
"displayOrder": 2,
"parentId": null,
"isDefault": false,
"isActive": true,
"description": "Order has been confirmed",
"metadata": null
}
],
"paymentMethods": [
{
"id": 1,
"code": "CASH",
"value": "Cash",
"displayOrder": 1,
"parentId": null,
"isDefault": true,
"isActive": true,
"description": null,
"metadata": "{\"icon\":\"💵\",\"color\":\"#4CAF50\"}"
}
]
}
}
Caching Strategy
Cache Key Format
Format: lookups:{storedProcedure}:{module}:{context}
Examples:
lookups:restaurant.getlookups:restaurant:generallookups:inventory.getlookups:warehouse:defaultlookups:hr.getlookups:employees:active
Benefits:
- Unique per API and context
- Easy to invalidate specific lookups
- Supports multi-tenant scenarios
Cache Configuration
In-Memory Cache (Single Server)
builder.Services.AddMemoryCache();
builder.Services.AddMemoryCacheService();
Distributed Cache (Multi-Server)
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = configuration.GetConnectionString("Redis");
});
builder.Services.AddDistributedCacheService();
Caching Behavior
- Default TTL: 30 minutes
- Cache Type: Configurable (in-memory or distributed)
- Cache Invalidation: Manual via
RefreshLookupsAsync()(removes specific cache key) - Cache Miss: Query hits database and populates cache
- Cache Hit: Returns data from cache (< 1ms)
Performance Considerations
Caching Performance
- Cache hit: < 1ms response time
- Cache miss: SP execution time + mapping time
- Cache expiration: 30 minutes default
Database Performance
- Query Type: Stored procedures (optimized)
- Connection: Reuses existing
IUnitOfWorkconnection - Result Mapping: Efficient DataTable → LINQ projection
Scalability
- In-Memory Cache: Good for single-server deployments
- Distributed Cache: Required for multi-server/load-balanced scenarios
- Cache Warming: First request per key hits database
Error Handling
Strategy: Return Result<T, DomainError> pattern
Benefits:
- ✅ Type-safe error handling
- ✅ No exceptions for business logic errors
- ✅ Consistent with Acontplus patterns
- ✅ Easy to map to HTTP responses
Error Codes:
LOOKUPS_GET_ERROR- Error retrieving lookupsLOOKUPS_REFRESH_ERROR- Error refreshing cacheLOOKUPS_EMPTY- No data returned from query
Migration Checklist
From Existing Code
✅ Update Dependencies
- Ensure your API references
Acontplus.ServicesNuGet package - Ensure your API references
Acontplus.InfrastructureNuGet package - Ensure your API references
Acontplus.CoreNuGet package
- Ensure your API references
✅ Register Services
- Add cache service registration
- Add lookup service registration
✅ Update/Create Stored Procedure
- Ensure it returns required columns
- Test stored procedure returns data correctly
✅ Update Controller
- Inject
ILookupService - Update GET endpoint
- Add refresh endpoint
- Inject
✅ Remove Old Code
- Remove old
LookupServiceclass (if exists in your API) - Remove old
ILookupServiceinterface (if exists in your API) - Remove old
LookupItemDTO (if exists in your API) - Remove
ConcurrentDictionarycaching logic
- Remove old
✅ Testing
- Unit test: Service registration
- Integration test: GET lookups endpoint
- Integration test: Refresh lookups endpoint
- Integration test: Cache is working
- Load test: Multiple concurrent requests
Security Considerations
SQL Injection
- ✅ Uses parameterized stored procedures
- ✅ Filter values are passed as parameters
- ✅ No dynamic SQL construction
Data Access
- ✅ Respects existing
IUnitOfWorksecurity - ✅ No elevation of privileges
- ✅ Uses application's database context
Cache Poisoning
- ✅ Cache keys are deterministic
- ✅ No user input in cache keys (normalized)
- ✅ Cache expiration prevents stale data
Troubleshooting
Cache not working
- Verify
ICacheServiceis registered - Check logs for cache errors
- Ensure Redis is running (if using distributed cache)
Missing columns
- Check stored procedure returns all required columns
- Verify column names match exactly (case-sensitive in PostgreSQL)
Slow performance
- Add indexes to lookup tables
- Check stored procedure execution plan
- Consider cache warming on startup
Memory issues
- Use distributed cache instead of in-memory
- Reduce cache TTL
- Limit lookup data size
Live Demo
See apps/src/Demo.Api/Endpoints/Core/LookupEndpoints.cs for a working example.
References
- Live Example:
apps/src/Demo.Api- Complete working implementation - Package:
Acontplus.Services- Service implementation - DTO:
Acontplus.Core/Dtos/Responses/LookupItem.cs- Shared DTO
Caching Service
Requires:
Acontplus.Infrastructurepackage
public class ProductService
{
private readonly ICacheService _cache;
public ProductService(ICacheService cache) => _cache = cache;
public async Task<Product?> GetProductAsync(int id)
{
var cacheKey = $"product:{id}";
// Async caching with factory pattern
return await _cache.GetOrCreateAsync(
cacheKey,
async () => await _repository.GetByIdAsync(id),
TimeSpan.FromMinutes(30)
);
}
}
Device Detection
public class ProductController : ControllerBase
{
private readonly IDeviceDetectionService _deviceDetection;
[HttpGet("products")]
public async Task<IActionResult> GetProducts()
{
var userAgent = Request.Headers.UserAgent.ToString();
var capabilities = _deviceDetection.GetDeviceCapabilities(userAgent);
var products = capabilities.IsMobile
? await _productService.GetMobileProductsAsync()
: await _productService.GetDesktopProductsAsync();
return Ok(products);
}
}
Request Context Management
public class OrderController : ControllerBase
{
private readonly IRequestContextService _requestContext;
[HttpPost("orders")]
public async Task<IActionResult> CreateOrder(CreateOrderRequest request)
{
var correlationId = _requestContext.GetCorrelationId();
var tenantId = _requestContext.GetTenantId();
var clientId = _requestContext.GetClientId();
_logger.LogInformation("Creating order for tenant {TenantId}", tenantId);
return Ok(new { OrderId = request.OrderId, CorrelationId = correlationId });
}
}
Context Extensions
public class AdvancedController : ControllerBase
{
[HttpGet("context-info")]
public IActionResult GetContextInfo()
{
// HTTP context extensions
var userAgent = HttpContext.GetUserAgent();
var ipAddress = HttpContext.GetClientIpAddress();
var requestPath = HttpContext.GetRequestPath();
// Claims principal extensions
var userId = User.GetUserId();
var email = User.GetEmail();
var roles = User.GetRoles();
var isAdmin = User.HasRole("admin");
return Ok(new
{
Request = new { UserAgent = userAgent, IpAddress = ipAddress, Path = requestPath },
User = new { UserId = userId, Email = email, Roles = roles, IsAdmin = isAdmin }
});
}
}
Security Headers
public class SecurityController : ControllerBase
{
private readonly ISecurityHeaderService _securityHeaders;
[HttpGet("headers")]
public IActionResult GetRecommendedHeaders()
{
var headers = _securityHeaders.GetRecommendedHeaders(isDevelopment: false);
var cspNonce = _securityHeaders.GenerateCspNonce();
return Ok(new { Headers = headers, CspNonce = cspNonce });
}
}
🔒 Security & Authorization
Authorization Policies
[Authorize(Policy = "RequireClientId")]
[HttpGet("secure")]
public IActionResult SecureEndpoint()
{
return Ok("Access granted");
}
[Authorize(Policy = "RequireTenant")]
[HttpGet("tenant-data")]
public IActionResult GetTenantData()
{
return Ok("Tenant-specific data");
}
[Authorize(Policy = "MobileOnly")]
[HttpGet("mobile-only")]
public IActionResult MobileOnlyEndpoint()
{
return Ok("Mobile access only");
}
📊 Health Checks
Access comprehensive health information at /health:
{
"status": "Healthy",
"results": {
"request-context": {
"status": "Healthy",
"description": "Request context service is fully operational"
},
"security-headers": {
"status": "Healthy",
"description": "Security header service is operational"
},
"device-detection": {
"status": "Healthy",
"description": "Device detection service is fully operational"
},
"cache": {
"status": "Healthy",
"description": "Cache service is fully operational",
"data": {
"totalEntries": 150,
"hitRatePercentage": 85.5
}
}
}
}
🔐 JWT Authentication Usage
Quick Start
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add JWT authentication with one line
builder.Services.AddJwtAuthentication(builder.Configuration);
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Configuration
{
"JwtSettings": {
"Issuer": "https://auth.acontplus.com",
"Audience": "api.acontplus.com",
"SecurityKey": "your-super-secret-key-at-least-32-characters-long",
"ClockSkew": "5",
"RequireHttps": "true"
}
}
Controller Example
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class SecureController : ControllerBase
{
[HttpGet("data")]
public IActionResult GetSecureData()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var email = User.FindFirst(ClaimTypes.Email)?.Value;
return Ok(new {
Message = "Secure data accessed",
UserId = userId,
Email = email
});
}
}
📚 API Reference
Core Services
IRequestContextService- Request context management and correlationISecurityHeaderService- HTTP security headers and CSP managementIDeviceDetectionService- Device type detection and capabilities
Configuration
RequestContextConfiguration- Request context and security settingsJwtSettings- JWT authentication configuration
Middleware
RequestContextMiddleware- Request context extractionCspNonceMiddleware- CSP nonce generationApiExceptionMiddleware- Global exception handling
🤝 Contributing
When adding new features:
- Follow the established patterns (Services, Filters, Policies)
- Add comprehensive logging
- Include functional health checks for new services
- Update this documentation with configuration examples
- Add unit tests for new functionality
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
📋 Package Comparison
| Feature | Acontplus.Services | Acontplus.Infrastructure |
|---|---|---|
| Request Context | ✅ | ❌ |
| Security Headers | ✅ | ❌ |
| Device Detection | ✅ | ❌ |
| JWT Authentication | ✅ | ❌ |
| Authorization Policies | ✅ | ❌ |
| Caching | ❌ | ✅ |
| Circuit Breaker | ❌ | ✅ |
| Retry Policies | ❌ | ✅ |
| HTTP Client Factory | ❌ | ✅ |
| Rate Limiting | ❌ | ✅ |
🎯 Best Practices
✅ Do's
- Use
AddApplicationServices()for application-level concerns - Use
AddInfrastructureServices()for infrastructure concerns - NEW: Let DomainExceptions bubble up for simpler code
- NEW: Use Result pattern for complex workflows
- Always validate client IDs and tenant IDs in multi-tenant scenarios
- Configure CSP policies carefully to avoid breaking functionality
- Monitor health check endpoints regularly
- Use correlation IDs for request tracking across services
❌ Don'ts
- Don't disable security headers in production
- Don't use weak JWT security keys (minimum 32 characters)
- Don't expose internal errors in API responses
- Don't cache sensitive user data
- Don't ignore health check failures
- Don't use generic cache keys
- NEW: Don't catch and swallow DomainExceptions (let middleware handle them)
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Acontplus.Core (>= 2.1.1)
- Microsoft.ApplicationInsights.AspNetCore (>= 2.23.0)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 10.0.1)
- Microsoft.Extensions.Caching.Memory (>= 10.0.1)
- Microsoft.Extensions.Configuration (>= 10.0.1)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.1)
- Microsoft.Extensions.Configuration.Json (>= 10.0.1)
- Microsoft.Extensions.DependencyInjection (>= 10.0.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.1)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.1)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.1)
- Microsoft.Extensions.Options (>= 10.0.1)
- Microsoft.IdentityModel.Tokens (>= 8.15.0)
- NetEscapades.AspNetCore.SecurityHeaders (>= 1.3.1)
- System.Drawing.Common (>= 10.0.1)
- System.IdentityModel.Tokens.Jwt (>= 8.15.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.1.6 | 23 | 12/25/2025 |
| 2.1.5 | 58 | 12/23/2025 |
| 2.1.4 | 197 | 12/11/2025 |
| 2.1.3 | 293 | 12/7/2025 |
| 2.1.2 | 156 | 12/5/2025 |
| 2.1.1 | 196 | 12/4/2025 |
| 2.1.0 | 191 | 12/3/2025 |
| 2.0.2 | 181 | 11/26/2025 |
| 2.0.1 | 177 | 11/26/2025 |
| 2.0.0 | 183 | 11/23/2025 |
| 1.6.3 | 387 | 11/17/2025 |
| 1.6.2 | 387 | 11/17/2025 |
| 1.6.1 | 334 | 11/17/2025 |
| 1.6.0 | 267 | 11/10/2025 |
| 1.5.19 | 187 | 11/5/2025 |
| 1.5.18 | 193 | 11/5/2025 |
| 1.5.17 | 174 | 10/23/2025 |
| 1.5.16 | 176 | 9/26/2025 |
| 1.5.15 | 186 | 9/25/2025 |
| 1.5.14 | 205 | 9/25/2025 |
| 1.5.13 | 177 | 9/24/2025 |
| 1.5.12 | 231 | 9/14/2025 |
| 1.5.11 | 231 | 9/14/2025 |
| 1.5.10 | 228 | 9/14/2025 |
| 1.5.9 | 185 | 9/10/2025 |
| 1.5.8 | 185 | 9/9/2025 |
| 1.5.7 | 195 | 9/4/2025 |
| 1.5.6 | 212 | 8/24/2025 |
| 1.5.5 | 172 | 8/21/2025 |
| 1.5.4 | 171 | 8/19/2025 |
| 1.5.3 | 196 | 8/13/2025 |
| 1.5.2 | 179 | 8/13/2025 |
| 1.5.1 | 180 | 8/11/2025 |
| 1.5.0 | 173 | 8/11/2025 |
| 1.4.4 | 178 | 8/8/2025 |
| 1.4.3 | 175 | 8/8/2025 |
| 1.4.2 | 253 | 8/7/2025 |
| 1.4.1 | 258 | 8/7/2025 |
| 1.4.0 | 250 | 8/7/2025 |
| 1.3.2 | 264 | 8/5/2025 |
| 1.3.1 | 587 | 7/23/2025 |
| 1.3.0 | 126 | 7/18/2025 |
| 1.2.0 | 179 | 7/14/2025 |
| 1.1.4 | 177 | 7/14/2025 |
| 1.1.3 | 131 | 7/11/2025 |
| 1.1.2 | 130 | 7/11/2025 |
| 1.1.1 | 178 | 7/10/2025 |
| 1.1.0 | 193 | 7/10/2025 |
| 1.0.12 | 168 | 7/10/2025 |
| 1.0.11 | 180 | 7/9/2025 |
| 1.0.10 | 184 | 7/9/2025 |
| 1.0.9 | 180 | 7/6/2025 |
| 1.0.8 | 181 | 7/6/2025 |
| 1.0.7 | 181 | 7/6/2025 |
| 1.0.6 | 127 | 7/4/2025 |
| 1.0.5 | 185 | 7/2/2025 |
| 1.0.4 | 184 | 7/2/2025 |
| 1.0.3 | 187 | 7/2/2025 |
| 1.0.2 | 187 | 7/1/2025 |
v2.0.0: Major refactoring - Separated infrastructure concerns to Acontplus.Infrastructure. Focused on application-level services: JWT authentication, context management, security headers, authorization policies, and ASP.NET Core middleware.