Skip to content

andrei-m-code/halifax

Repository files navigation

Halifax Service Foundation

Simplistic libraries for complex projects. Halifax eliminates boilerplate in .NET API services — standardized responses, JWT auth, configuration, logging, and more — so you can focus on business logic.

CI NuGet

Package NuGet Description
Halifax.Api NuGet ASP.NET Core integration, middleware, Swagger
Halifax.Core NuGet JWT, config, validation, crypto, logging, JSON
Halifax.Domain NuGet Response models, exceptions, pagination
Halifax.Http NuGet Typed HttpClient with resilience policies
Halifax.Excel NuGet Excel/CSV import and export

Features

  • Standardized API responses — consistent ApiResponse<T> wrapper for all endpoints
  • Exception handling — throw typed exceptions, get proper HTTP status codes automatically
  • JWT authentication — configure auth in one line, create and validate tokens easily
  • Environment configuration — load .env files, map to strongly-typed classes/records
  • Input validation — fluent Guard helpers for common checks
  • Correlation IDs — automatic X-Correlation-Id propagation across services
  • HTTP client base class — typed HttpClient with automatic error mapping and resilience policies
  • OpenAPI + Scalar UI — Swagger docs with interactive API explorer out of the box
  • Logging — Serilog-based structured logging
  • CORS — configurable cross-origin policy
  • Excel/CSV — import and export with column mapping
  • Cryptography — AES-256 encrypt/decrypt helpers
  • Short IDs — thread-safe random ID generation
  • Nullable reference types — fully annotated across all packages

Quick Start

dotnet add package Halifax.Api
using Halifax.Api;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHalifax();

var app = builder.Build();
app.UseHalifax();
app.Run("https://*:5000");

This gives you controller routing, exception handling, Swagger, Scalar UI, CORS, correlation IDs, and structured logging. Explore the Peggy's Cove sample project for a full working example.

API Responses

All endpoints return a consistent format using ApiResponse:

// Return data
return ApiResponse.With(user);

// Return empty success
return ApiResponse.Empty;

Response format:

{
  "data": { ... },
  "success": true,
  "error": null
}

On error:

{
  "data": null,
  "success": false,
  "error": {
    "type": "HalifaxNotFoundException",
    "message": "User not found"
  }
}

Pagination

[HttpGet]
public ApiResponse<Paging<UserDto>> GetUsers([FromQuery] PagingQuery query)
{
    var items = db.Users.Skip(query.Skip).Take(query.Take).ToList();
    var total = db.Users.Count();
    return ApiResponse.With(new Paging<UserDto>(items, query.Skip, query.Take, total));
}

PagingQuery binds Skip, Take, OrderBy, and OrderDirection from query string parameters.

Exceptions

Throw typed exceptions anywhere in your code — the middleware handles the HTTP response:

Exception HTTP Status
HalifaxException 400 Bad Request
HalifaxNotFoundException 404 Not Found
HalifaxUnauthorizedException 401 Unauthorized
var user = await db.Users.FindAsync(id);
if (user == null)
    throw new HalifaxNotFoundException("User not found");

For advanced scenarios, override DefaultExceptionHandler or register your own IExceptionHandler.

Configuration

Halifax loads environment variables from .env files automatically. Define a class or record matching your variable names:

AppSettings__ConnectionString=localhost
AppSettings__HttpTimeout=120
record AppSettings(string ConnectionString, int HttpTimeout);

Register during startup:

builder.Services.AddHalifax(h => h.AddSettings<AppSettings>());

Settings are registered as singletons — inject them into controllers and services, or access them directly:

var settings = Env.GetSection<AppSettings>();

Supported types: string, primitives, DateTime, TimeSpan, Guid, and their nullable variants.

JWT Authentication

Enable authentication with one call:

builder.Services.AddHalifax(h => h
    .ConfigureAuthentication("your_jwt_secret_min_16_chars",
        validateAudience: false,
        validateIssuer: false,
        requireExpirationTime: false));

All non-[AllowAnonymous] endpoints now require Authorization: Bearer {token}.

Creating Tokens

var claims = new List<Claim>
{
    new("sub", user.Id.ToString()),
    new("email", user.Email),
    new("role", user.Role)
};

var token = Jwt.Create("your_jwt_secret", claims, DateTime.UtcNow.AddDays(30));

Reading Tokens

var principal = Jwt.Read("your_jwt_secret", token);

Claims-Based Authorization

Create custom authorization filters by extending ClaimsAuthorizeFilterAttribute:

class AdminOnly : ClaimsAuthorizeFilterAttribute
{
    protected override bool IsAuthorized(ActionExecutingContext context, List<Claim> claims)
    {
        claims.ClaimExpected("role", "admin");
        return true;
    }
}

[HttpGet("admin")]
[AdminOnly]
public ApiResponse GetAdminData() => ApiResponse.With("secret");

Available claim extensions: ClaimExpected, ClaimNotNullOrWhiteSpace, ClaimIsEmail, ClaimIsInt, ClaimIsDouble, ClaimIsEnum<T>, ClaimIsGuid, ClaimIsBoolean, ClaimIs<T> (for any IParsable<T> type).

Correlation IDs

Halifax automatically propagates correlation IDs across your services. The CorrelationIdMiddleware is enabled by default when you call UseHalifax():

  • Reads the incoming X-Correlation-Id header, or generates a new GUID if absent
  • Sets HttpContext.TraceIdentifier for logging and downstream use
  • Adds X-Correlation-Id to the response headers

For service-to-service calls, the CorrelationIdDelegatingHandler automatically forwards the correlation ID to outgoing HTTP requests made through HalifaxHttpClient.

Validation

Guard provides fluent validation that throws HalifaxException (400) on failure:

Guard.NotNullOrWhiteSpace(request.Name, nameof(request.Name));
Guard.Email(request.Email);
Guard.Length(request.Password, nameof(request.Password), lower: 8, upper: 64);
Guard.Range(request.Age, nameof(request.Age), from: 18, to: 120);
Guard.Url(request.Website, nameof(request.Website));
Guard.Ensure(request.AcceptedTerms, "Terms must be accepted");
Guard.NotNull(request.Address, nameof(request.Address));
Guard.NotEmptyList(request.Tags, nameof(request.Tags));
Guard.Color(request.Theme, nameof(request.Theme));

HTTP Client

Create typed HTTP clients for service-to-service communication by extending HalifaxHttpClient:

public class PaymentClient(HttpClient http) : HalifaxHttpClient(http)
{
    public async Task<PaymentDto> GetAsync(string id)
    {
        var msg = CreateMessage(HttpMethod.Get, $"/api/payments/{id}");
        return await SendAsync<PaymentDto>(msg);
    }

    public async Task<HttpStatusCode> CreateAsync(CreatePaymentRequest request)
    {
        var msg = CreateMessage(HttpMethod.Post, "/api/payments", request);
        return await SendAsync(msg);
    }
}

Register with optional defaults:

services.AddHalifaxHttpClient<PaymentClient>(
    defaultBaseUrl: "https://payments.api.com",
    defaultBearerToken: token);

Error responses (400, 401, 404) from downstream services are automatically mapped to the corresponding Halifax exceptions.

Resilience Policies

Add retry, circuit breaker, and timeout policies using the built-in resilience support:

// Standard resilience (retry + circuit breaker + timeout)
services.AddHalifaxHttpClientWithResilience<PaymentClient>(
    defaultBaseUrl: "https://payments.api.com");

// Or get the IHttpClientBuilder for custom configuration
services.AddHalifaxHttpClientBuilder<PaymentClient>(
    defaultBaseUrl: "https://payments.api.com",
    defaultBearerToken: null,
    configure: null)
    .AddStandardResilienceHandler(options =>
    {
        options.Retry.MaxRetryAttempts = 3;
    });

Excel & CSV

dotnet add package Halifax.Excel
var converter = new ExcelConverter<Person>();
converter.AddMapping("Full Name", p => p.Name);
converter.AddMapping("Age", p => p.Age);

// Write
using var stream = new MemoryStream();
await converter.WriteExcelAsync(stream, people, "Sheet1");
await converter.WriteCsvAsync(stream, people);

// Read (auto-detects format from content type)
var records = await converter.ReadAsync(fileStream, contentType);

Utilities

Cryptography

AES-256 encryption:

var encrypted = Crypto.Encrypt("secret", "sensitive data");
var decrypted = Crypto.Decrypt("secret", encrypted);
Crypto.TryDecrypt("secret", encrypted, out var result);

Short IDs

Thread-safe random ID generation:

var id = ShortId.Create();              // e.g. "kX9mBnQ"
var id = ShortId.Create(length: 12);    // longer ID
var id = ShortId.Create(useNumbers: false); // letters only

JSON

Pre-configured serialization (camelCase, case-insensitive, enums as strings, UTC dates):

var json = Json.Serialize(obj);
var obj = Json.Deserialize<MyType>(json);
Json.TryDeserialize<MyType>(json, out var result);

DateTime Helpers

DateTimeHelper.ValidateRange(from, to);           // throws if from > to
DateTimeHelper.IsIn(from, to, pointInTime);        // true if within range
DateTime? date = DateTime.UtcNow;
date.ToIsoFormat();                                // "2024-03-15T10:30:00Z"

Logging

Global structured logging via Serilog:

L.Info("User created", userId);
L.Warning("Timeout exceeded");
L.Error(exception, "Failed to process");

Version Endpoint

Halifax exposes a built-in GET /halifax/version endpoint that returns the loaded Halifax assembly versions:

{
  "data": [
    { "name": "Halifax.Api", "version": "5.2.1" },
    { "name": "Halifax.Core", "version": "5.1.1" }
  ],
  "success": true
}

Full Configuration Example

builder.Services.AddHalifax(h => h
    .SetName("My Service")
    .AddSettings<AppSettings>()
    .AddSettings<DatabaseSettings>()
    .ConfigureAuthentication(jwtSecret, false, false, false)
    .ConfigureCors(cors => cors
        .AllowAnyHeader()
        .AllowAnyMethod()
        .WithOrigins("https://myapp.com"))
    .ConfigureOpenApi(swagger => { /* customize Swashbuckle */ })
    .ConfigureJson(opts => { /* customize System.Text.Json */ }));

Package Overview

Package Dependencies
Halifax.Domain None
Halifax.Core Halifax.Domain, Serilog, System.IdentityModel.Tokens.Jwt
Halifax.Api Halifax.Core, Swashbuckle, Scalar, JwtBearer
Halifax.Http Halifax.Core, Microsoft.Extensions.Http.Resilience
Halifax.Excel CsvHelper, ExcelMapper, NPOI

All packages target .NET 10 with nullable reference types enabled.

MIT License

Copyright (c) 2020 Andrei M

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Packages

 
 
 

Contributors

Languages