Core utilities, extensions, validation, and common components for Nera applications
$ dotnet add package Nera.Lib.CoreA comprehensive .NET 9 library providing core utilities, extensions, validation, CQRS patterns, logging, and common components for Nera applications.
dotnet add package Nera.Lib.Core
using Nera.Lib.Core.Builders;
using Nera.Lib.Core.Middlewares;
var builder = WebApplication.CreateBuilder(args);
// Configure Nera Core with fluent API
builder.Services
.AddNeraCore(builder.Configuration)
.WithCoreOptions(options =>
{
options.ApplicationName = "MyApp";
options.Version = "v1";
})
.WithMediatR()
.WithValidation()
.WithLogging()
.WithTimestampConverter()
.Build();
var app = builder.Build();
// Use Nera middleware for response transformation
app.UseMiddleware<ResultTransformationMiddleware>();
app.Run();
| Feature | Description |
|---|---|
| CQRS | MediatR integration with Command/Query separation |
| Validation | FluentValidation with automatic pipeline integration |
| Logging | Serilog with Elasticsearch, Console, and File sinks |
| Result Pattern | MethodResult<T> for consistent API responses |
| Pagination | Built-in pagination support with metadata |
| Exception Handling | Global exception handling with localized messages |
| JSON Converters | Unix timestamp converters for DateTime |
| Response Transformation | Automatic API response wrapping |
Use the fluent CoreBuilder API to configure services:
builder.Services
.AddNeraCore(builder.Configuration)
// Core application options
.WithCoreOptions(options =>
{
options.ApplicationName = "MyApp";
options.Version = "v1";
options.Environment = "Production";
options.AcceptedLanguages = "en-US,vi-VN";
})
// Database configuration
.WithDatabase(options =>
{
options.ConnectionString = "Host=localhost;...";
options.MaxPoolSize = 100;
options.CommandTimeout = 30;
})
// Or simply:
// .WithDatabase("Host=localhost;...")
// Redis caching
.WithRedis(options =>
{
options.ConnectionString = "localhost:6379";
options.InstanceName = "myapp:";
options.DefaultExpirationMinutes = 60;
})
// RabbitMQ messaging
.WithRabbitMq(options =>
{
options.ConnectionString = "amqp://guest:guest@localhost:5672";
options.ExchangeName = "my_exchange";
options.QueueName = "my_queue";
})
// Elasticsearch for logging
.WithElasticsearch(options =>
{
options.Url = "http://localhost:9200";
options.IndexFormat = "logs-{0:yyyy.MM.dd}";
})
// MediatR with behaviors
.WithMediatR()
// FluentValidation
.WithValidation()
// Serilog logging
.WithLogging(options =>
{
options.MinimumLevel = Serilog.Events.LogEventLevel.Information;
options.UseConsoleLogging = true;
options.UseFileLogging = true;
options.LogFilePath = "logs/app.log";
})
// JSON serialization with Unix timestamps
.WithTimestampConverter(useSeconds: false) // milliseconds by default
// Scan assemblies for handlers/validators
.WithAssemblies(typeof(Program).Assembly)
.Build();
All options can be configured via appsettings.json:
{
"NeraCore": {
"ApplicationName": "MyApp",
"Version": "v1",
"Environment": "Production",
"AcceptedLanguages": "en-US,vi-VN",
"AllowOrigin": "*"
},
"Database": {
"ConnectionString": "Host=localhost;Port=5432;Database=mydb;Username=postgres;Password=postgres",
"EnablePooling": true,
"MinPoolSize": 1,
"MaxPoolSize": 100,
"ConnectionTimeout": 30,
"CommandTimeout": 30
},
"Redis": {
"Enabled": true,
"ConnectionString": "localhost:6379",
"InstanceName": "myapp:",
"DefaultExpirationMinutes": 60,
"UseSsl": false
},
"RabbitMq": {
"Enabled": true,
"ConnectionString": "amqp://guest:guest@localhost:5672",
"ExchangeName": "my_exchange",
"QueueName": "my_queue",
"RoutingKey": "my_routing_key"
},
"Elasticsearch": {
"Enabled": true,
"Url": "http://localhost:9200",
"Username": "elastic",
"Password": "changeme",
"IndexFormat": "logs-{0:yyyy.MM.dd}"
},
"JsonSerialization": {
"UseUnixTimestamp": true,
"UseSeconds": false,
"UseCamelCase": true,
"IgnoreNullValues": true
},
"ResponseTransformation": {
"Enabled": true,
"IncludeTraceId": true,
"IncludeProcessingTime": true,
"ExcludePaths": ["/swagger*", "/health*"]
}
}
Environment variables take precedence over configuration files:
| Variable | Description | Default |
|---|---|---|
APPLICATION_NAME | Application name | Nera.Lib.Core |
APP_VERSION | Application version | v1 |
ENV_CONNECTION_STRING | Database connection string | - |
ENV_REDIS_CONNECTION_STRING | Redis connection string | localhost:6379 |
ENV_REDIS_CACHE_ENABLE | Enable Redis caching | false |
ENV_RABBITMQ_CONNECTION_STRING | RabbitMQ connection string | - |
ENV_ES_URL | Elasticsearch URL | http://localhost:9200 |
ENV_ES_LOGS_ENABLE | Enable Elasticsearch logging | false |
ENV_ACCEPT_LANGUAGES | Accepted languages | en-US |
ALLOW_ORIGIN | CORS allowed origins | * |
using Nera.Lib.Core.CQRS;
using Nera.Lib.Core.Results.ActionResult;
// Define a command
public record CreateUserCommand(string Name, string Email) : ICommand<MethodResult<UserDto>>;
// Implement the handler
public class CreateUserCommandHandler : ICommandHandler<CreateUserCommand, MethodResult<UserDto>>
{
private readonly IUserRepository _repository;
public CreateUserCommandHandler(IUserRepository repository)
{
_repository = repository;
}
public async Task<MethodResult<UserDto>> Handle(
CreateUserCommand request,
CancellationToken cancellationToken)
{
var user = new User { Name = request.Name, Email = request.Email };
await _repository.AddAsync(user, cancellationToken);
var result = new MethodResult<UserDto>();
result.AddResult(user.Adapt<UserDto>());
return result;
}
}
// Define a query
public record GetUserByIdQuery(Guid Id) : IQuery<MethodResult<UserDto>>;
// Implement the handler
public class GetUserByIdQueryHandler : IQueryHandler<GetUserByIdQuery, MethodResult<UserDto>>
{
private readonly IUserRepository _repository;
public async Task<MethodResult<UserDto>> Handle(
GetUserByIdQuery request,
CancellationToken cancellationToken)
{
var user = await _repository.GetByIdAsync(request.Id, cancellationToken);
var result = new MethodResult<UserDto>();
if (user == null)
{
result.AddError("NOT_FOUND", "User not found", "User not found", new ErrorResult());
return result;
}
result.AddResult(user.Adapt<UserDto>());
return result;
}
}
Validators are automatically discovered and integrated into the MediatR pipeline:
using FluentValidation;
using Nera.Lib.Core.Validation;
public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
public CreateUserCommandValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Name is required")
.MaximumLength(100).WithMessage("Name must not exceed 100 characters");
RuleFor(x => x.Email)
.NotEmpty().WithMessage("Email is required")
.EmailAddress().WithMessage("Invalid email format");
}
}
using Nera.Lib.Core.Validation;
public class MyValidator : AbstractValidator<MyCommand>
{
public MyValidator()
{
// Use common validation rules
RuleFor(x => x.PhoneNumber).ValidPhoneNumber();
RuleFor(x => x.Url).ValidUrl();
RuleFor(x => x.Date).ValidDate();
}
}
using Nera.Lib.Core.Results.ActionResult;
// Success result
var result = new MethodResult<UserDto>();
result.AddResult(userDto);
result.AddMessage("User created successfully");
// Error result
var errorResult = new MethodResult<UserDto>();
errorResult.AddError(
code: "USER_EXISTS",
message: "User already exists",
errorMessage: "A user with this email already exists",
errorResult: new ErrorResult { Details = "email@example.com" }
);
// For operations without return data
var result = new VoidMethodResult();
result.AddMessage("Operation completed successfully");
using Nera.Lib.Core.Results.Pagination;
// Create paginated result
var paginatedResult = new PaginatedResult<UserDto>(
page: 1,
pageSize: 10,
count: totalCount,
data: users
);
// Convert entity to DTO
var dtoResult = paginatedResult.ToResult<UserResponseDto>();
Automatically wraps API responses in a standard format:
var app = builder.Build();
// Add before other middleware
app.UseMiddleware<ResultTransformationMiddleware>();
app.MapControllers();
Features:
builder.Services
.AddNeraCore(builder.Configuration)
.WithLogging(options =>
{
options.MinimumLevel = LogEventLevel.Information;
options.UseConsoleLogging = true;
options.UseFileLogging = true;
options.LogFilePath = "logs/app.log";
})
.Build();
.WithLogging(options =>
{
options.UseElasticsearch = true;
options.ElasticsearchUrl = "http://localhost:9200";
options.IndexFormat = "myapp-logs-{0:yyyy.MM.dd}";
})
using Nera.Lib.Core.Logging;
// Development
.WithLogging(_ => SerilogExtensions.CreateDevelopmentOptions())
// Production
.WithLogging(_ => SerilogExtensions.CreateProductionOptions())
// Testing
.WithLogging(_ => SerilogExtensions.CreateTestingOptions())
The following data is automatically masked in logs:
Configure DateTime to be serialized as Unix timestamps:
builder.Services
.AddNeraCore(builder.Configuration)
.WithTimestampConverter(useSeconds: false) // milliseconds
.Build();
// Or with full options
.WithJsonOptions(options =>
{
options.UseUnixTimestamp = true;
options.UseSeconds = false; // Use milliseconds
options.UseCamelCase = true;
options.IgnoreNullValues = true;
})
using Nera.Lib.Core.Extensions;
// Get pre-configured JsonSerializerOptions
var options = JsonOptionsExtensions.CreateNeraJsonOptions(useSeconds: false);
// Or configure MVC JSON options
builder.Services.AddNeraJsonOptions(useSeconds: false);
The library accepts both formats in requests:
// ISO 8601 format
{ "createdAt": "2025-12-03T10:30:00Z" }
// Unix timestamp (milliseconds)
{ "createdAt": 1733224200000 }
// Unix timestamp (seconds) - auto-detected
{ "createdAt": 1733224200 }
All DateTime values are returned as Unix timestamps (milliseconds):
{
"data": {
"id": "123",
"createdAt": 1733224200000,
"updatedAt": 1733310600000
},
"_metadata": {
"timestamp": 1733224200000
}
}
using Nera.Lib.Core.Exceptions;
// 400 Bad Request
throw new BadRequestException("Invalid input");
throw new InvalidValidatorException("Validation failed");
throw new BusinessRuleViolationException("Business rule violated");
// 401 Unauthorized
throw new UnAuthorizeException("Authentication required");
// 403 Forbidden
throw new ForbidException("Access denied");
// 404 Not Found
throw new NotFoundException("Resource not found");
// 500 Internal Server Error
throw new InternalServerException("An error occurred");
| Exception | Status Code |
|---|---|
BadRequestException | 400 |
InvalidValidatorException | 400 |
BusinessRuleViolationException | 400 |
UnAuthorizeException | 401 |
ForbidException | 403 |
NotFoundException | 404 |
InternalServerException | 500 |
Error messages are automatically translated based on Accept-Language header:
// Add error resources
// Resources/CommonErrors-en.json
{
"USER_NOT_FOUND": "User not found"
}
// Resources/CommonErrors-vi.json
{
"USER_NOT_FOUND": "Không tìm thấy người dùng"
}
{
"status": "Success",
"message": "Success",
"data": {
"id": "123",
"name": "John Doe",
"createdAt": 1733224200000
},
"_metadata": {
"traceId": "0HNEFIIB8HKSG:00000001",
"processingTime": 45,
"timestamp": 1733224200000
}
}
{
"status": "Success",
"message": "Success",
"data": [
{ "id": "1", "name": "User 1" },
{ "id": "2", "name": "User 2" }
],
"_metadata": {
"traceId": "0HNEFIIB8HKSG:00000001",
"processingTime": 120,
"timestamp": 1733224200000,
"pagination": {
"page": 1,
"pageSize": 10,
"count": 2,
"total": 50,
"totalPages": 5,
"hasPreviousPage": false,
"hasNextPage": true
}
}
}
{
"status": "Fail",
"message": "Validation failed",
"data": null,
"error": {
"code": "VALIDATION_ERROR",
"message": "One or more validation errors occurred",
"details": [
{ "field": "email", "message": "Invalid email format" }
]
},
"_metadata": {
"traceId": "0HNEFIIB8HKSG:00000001",
"processingTime": 5,
"timestamp": 1733224200000
}
}
See the Examples folder for complete working examples.
We welcome contributions! Please see our Contributing Guide for details.
This project is licensed under the MIT License - see the LICENSE file for details.
Developed by Nextera Systems for building robust .NET applications.
Version: 1.0.0
Target Framework: .NET 9.0
Repository: https://github.com/nextera-systems/nera-lib-core