Skip to content

Latest commit

 

History

154 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Roadbed
Roadbed


A comprehensive framework for building resilient, data-driven C# applications with standardized patterns for data access, HTTP communication, messaging, and API integration.

Overview

Roadbed provides a cohesive set of libraries that work together to accelerate development of maintainable .NET applications. Built on modern .NET patterns including dependency injection, structured logging, and async/await, Roadbed eliminates boilerplate code while enforcing best practices.

Key Features

  • Auto-Discovery Module Registration - Automatic dependency injection setup across assemblies
  • Repository & Entity Patterns - Structured CRUD operations with business logic separation
  • HTTP Client Wrapper - Resilient API calls with automatic retry logic and compression
  • Standardized Messaging - Consistent message envelopes for event-driven architectures
  • Data Access Abstractions - Database-agnostic interfaces with SQLite implementation
  • Base Classes with Logging - Performance-optimized logging built into base classes
  • Job Scheduling - Automatic job discovery and scheduling with Quartz.NET, including persistent / clustered backends
  • OpenTelemetry-First Logging - Structured MEL output persisted to MySQL/SQLite, with first-class activity/run-instance and lineage tracking
  • KeePass-Backed Secrets - Self-contained .kdbx reader for surfacing secrets into typed options at startup
  • File I/O Utilities - Type-safe CSV operations with custom mappers
  • SDK Development Tools - Patterns for building type-safe API client libraries

Architecture

┌────────────────────────────────────────────────────────────────────────────┐
│                            Your Application                                 │
└──────────────────────────────────┬──────────────────────────────────────────┘
                                   │
       ┌──────────┬──────────┬─────┴──────┬──────────────┬──────────────────┐
       │          │          │            │              │                  │
  ┌────▼────┐ ┌──▼─────┐ ┌──▼─────┐ ┌────▼─────┐  ┌─────▼──────┐  ┌────────▼────────┐
  │ Roadbed │ │Roadbed │ │Roadbed │ │ Roadbed  │  │  Roadbed   │  │     Roadbed     │
  │  .Crud  │ │  .Net  │ │.Messag-│ │.Scheduli-│  │  .Logging  │  │ .Secrets.KeePass│
  │         │ │        │ │  ing   │ │   ng     │  │            │  │                 │
  └────┬────┘ └────────┘ └────────┘ └──────────┘  └─────┬──────┘  └─────────────────┘
       │                                                │
       └──────────────────┬─────────────────────────────┘
                          │
                  ┌───────▼────────┐         ┌────────────────┐
                  │  Roadbed.Data* │         │  Roadbed.IO*   │
                  │   (Data,       │         │  (IO, IO.Csv)  │
                  │    Dapper,     │         │                │
                  │    MySql,      │         └────────────────┘
                  │    Sqlite,     │
                  │    Postgresql) │
                  └────────┬───────┘
                           │
                  ┌────────▼──────┐
                  │    Roadbed    │
                  │    .Common    │
                  └───────────────┘
  • Roadbed.Data* collapses Roadbed.Data, Roadbed.Data.Dapper, Roadbed.Data.MySql, Roadbed.Data.Sqlite, and Roadbed.Data.Postgresql. The base package is the abstraction; the per-provider packages supply the executor and Dapper handlers.
  • Roadbed.IO* collapses Roadbed.IO (typed file abstractions) and Roadbed.IO.Csv (CSV reading/writing).
  • Roadbed.Crud and Roadbed.Logging are the only consumable-layer packages that sit on Roadbed.Data\*; the rest reach Roadbed.Common directly.

Package Ecosystem

For detailed documentation on each package:

Quick Start

1. Install Core Package

dotnet add package Roadbed.Common

2. Define an Entity

using Roadbed.Crud;

public sealed record FooRecord : BaseEntityRecord<long>
{
    public string? Name { get; set; }
    public string? Description { get; set; }
}

3. Implement a Repository

Repository interfaces and implementations are internal. The repository handles data access only.

using Microsoft.Extensions.Logging;
using Roadbed.Crud;
using Roadbed.Crud.Repositories.Async;

internal interface IFooRepository
    : IAsyncCrudlRepository<FooRecord, long>
{
}

internal sealed class FooRepository
    : BaseAsyncCrudlRepository<FooRecord, long>,
      IFooRepository
{
    public FooRepository(ILogger<FooRepository> logger)
        : base(logger)
    {
    }

    public override async Task<FooRecord> CreateAsync(
        FooRecord entity,
        CancellationToken cancellationToken = default)
    {
        // Data access implementation
        throw new NotImplementedException();
    }

    public override async Task<FooRecord?> ReadAsync(
        long id,
        CancellationToken cancellationToken = default)
    {
        // Data access implementation
        throw new NotImplementedException();
    }

    public override async Task<FooRecord> UpdateAsync(
        FooRecord entity,
        CancellationToken cancellationToken = default)
    {
        // Data access implementation
        throw new NotImplementedException();
    }

    public override async Task DeleteAsync(
        long id,
        CancellationToken cancellationToken = default)
    {
        // Data access implementation
        throw new NotImplementedException();
    }

    public override async Task<IList<FooRecord>> ListAsync(
        CancellationToken cancellationToken = default)
    {
        // Data access implementation
        throw new NotImplementedException();
    }
}

4. Implement a Service

Service classes are public sealed with dual constructors: a public constructor for consuming applications (resolves the repository via ServiceLocator) and an internal constructor for unit tests (accepts the repository directly via InternalsVisibleTo).

using Microsoft.Extensions.Logging;
using Roadbed;
using Roadbed.Crud.Services.Async;

public sealed class FooService
    : BaseAsyncCrudlService<FooRecord, long>
{
    /// <summary>
    /// Public constructor for consuming applications.
    /// </summary>
    public FooService(ILogger<FooService> logger)
        : base(ServiceLocator.GetService<IFooRepository>(), logger)
    {
    }

    /// <summary>
    /// Internal constructor for unit tests.
    /// </summary>
    internal FooService(
        IFooRepository repository,
        ILogger<FooService> logger)
        : base(repository, logger)
    {
    }

    /// <summary>
    /// Custom business logic beyond standard CRUDL operations.
    /// </summary>
    public async Task<FooRecord> CloneFooAsync(
        long sourceId,
        string newName,
        CancellationToken cancellationToken = default)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(newName);

        FooRecord? source = await this.ReadAsync(sourceId, cancellationToken);
        ArgumentNullException.ThrowIfNull(source);

        var clone = new FooRecord
        {
            Name = newName,
            Description = source.Description,
        };

        return await this.CreateAsync(clone, cancellationToken);
    }
}

5. Create a Module Installer

The installer registers only the internal repository (for ServiceLocator resolution). The public sealed service class is consumed directly by the application — it is not registered in DI.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Roadbed;

public sealed class InstallFooModule : IServiceCollectionInstaller
{
    public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
    {
        // Register repository for ServiceLocator resolution
        services.AddScoped<IFooRepository, FooRepository>();

        // Capture ServiceLocator snapshot for NuGet self-containment
        ServiceLocator.SetLocatorProvider(services.BuildServiceProvider());
    }
}

6. Application Startup

using Roadbed;

var builder = WebApplication.CreateBuilder(args);

// Auto-discovers and registers all IServiceCollectionInstaller implementations
builder.Services.InstallModulesInAppDomain(builder.Configuration);

var app = builder.Build();
app.Run();

7. Make HTTP Calls with NetHttpClient

using Microsoft.Extensions.Logging;
using Roadbed.Net;

public class BarApiClient : BaseClassWithLogging
{
    private readonly INetHttpClient _httpClient;

    public BarApiClient(
        INetHttpClient httpClient,
        ILogger<BarApiClient> logger)
        : base(logger)
    {
        ArgumentNullException.ThrowIfNull(httpClient);

        this._httpClient = httpClient;
    }

    public async Task<BarResponse?> GetBarAsync(
        string barId,
        CancellationToken cancellationToken = default)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(barId);

        var request = new NetHttpRequest
        {
            HttpEndPoint = new Uri($"https://api.example.com/bars/{barId}"),
            Method = HttpMethod.Get,
            Authentication = new NetHttpAuthentication
            {
                AuthenticationType = NetHttpAuthenticationType.Bearer,
                Value = "my-api-token",
            },
        };

        NetHttpResponse<BarResponse> response =
            await this._httpClient.MakeHttpRequestAsync<BarResponse>(
                request,
                cancellationToken);

        if (!response.IsSuccessStatusCode)
        {
            this.LogWarning(
                "Failed to get Bar {BarId}: {StatusCode}",
                barId,
                response.HttpStatusCode);

            return null;
        }

        return response.Data;
    }
}

Design Principles

Auto-Discovery Over Manual Registration

Roadbed uses IServiceCollectionInstaller to automatically discover and register services across assemblies, eliminating manual wiring in Program.cs.

Base Classes with Built-In Logging

BaseClassWithLogging provides performance-optimized logging methods that check log levels before formatting messages, preventing unnecessary string allocations.

// Good: No string formatting occurs when Debug logging is disabled
this.LogDebug("Processing {Count} items with {Size} bytes", items.Count, totalSize);

Repository Pattern with Service Delegation

Repositories handle data access and are internal. Services are public sealed and delegate to repositories, composing higher-level operations like ExistsAsync and UpsertAsync from repository primitives. This separation keeps concerns clear and code testable.

Dual Constructor Pattern

Service classes expose a public constructor (resolves dependencies via ServiceLocator) and an internal constructor (accepts dependencies directly for unit testing via InternalsVisibleTo). This enables NuGet packages to be self-contained while remaining fully testable.

Standardized Response Patterns

NetHttpResponse<T> provides consistent success/failure patterns across HTTP boundaries with built-in retry and backoff support via NetHttpRequest.

ServiceLocator for NuGet Self-Containment

While generally an anti-pattern, ServiceLocator enables NuGet packages to operate self-contained without requiring consumers to manually register internal dependencies.

Complete Example: Foo Service with External API

using Microsoft.Extensions.Logging;
using Roadbed;
using Roadbed.Crud;
using Roadbed.Crud.Services.Async;
using Roadbed.Net;

/// <summary>
/// Service for managing Foo entities with external API enrichment.
/// Inherits CRUDL operations and composes ExistsAsync/UpsertAsync
/// from repository primitives.
/// </summary>
public sealed class FooService
    : BaseAsyncCrudlService<FooRecord, long>
{
    private readonly INetHttpClient _httpClient;

    /// <summary>
    /// Public constructor for consuming applications.
    /// </summary>
    public FooService(
        INetHttpClient httpClient,
        ILogger<FooService> logger)
        : base(ServiceLocator.GetService<IFooRepository>(), logger)
    {
        ArgumentNullException.ThrowIfNull(httpClient);

        this._httpClient = httpClient;
    }

    /// <summary>
    /// Internal constructor for unit tests.
    /// </summary>
    internal FooService(
        IFooRepository repository,
        INetHttpClient httpClient,
        ILogger<FooService> logger)
        : base(repository, logger)
    {
        ArgumentNullException.ThrowIfNull(httpClient);

        this._httpClient = httpClient;
    }

    /// <summary>
    /// Creates a Foo entity enriched with data from an external API.
    /// </summary>
    public async Task<FooRecord> CreateEnrichedFooAsync(
        string name,
        CancellationToken cancellationToken = default)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(name);

        this.LogInformation("Creating enriched Foo: {Name}", name);

        // Fetch enrichment data from external API
        var request = new NetHttpRequest
        {
            HttpEndPoint = new Uri($"https://api.example.com/enrich/{name}"),
            Method = HttpMethod.Get,
            RetryPattern = new NetHttpRetryPattern
            {
                MaxAttempts = 2,
                DelayMultiplierInSeconds = 3,
            },
        };

        NetHttpResponse<FooEnrichmentDto> response =
            await this._httpClient.MakeHttpRequestAsync<FooEnrichmentDto>(
                request,
                cancellationToken);

        // Build entity with enrichment data (or defaults on failure)
        var foo = new FooRecord
        {
            Name = name,
            Description = response.IsSuccessStatusCode
                ? response.Data?.Description
                : "No enrichment data available",
        };

        // UpsertAsync checks ExistsAsync, then calls CreateAsync or UpdateAsync
        FooRecord result = await this.UpsertAsync(foo, cancellationToken);

        this.LogInformation("Foo created with Id: {Id}", result.Id);

        return result;
    }
}

Requirements

  • .NET 10.0+
  • Microsoft.Extensions.DependencyInjection
  • Microsoft.Extensions.Logging
  • Microsoft.Extensions.Configuration

About

.NET solution containing re-usable class libraries with utilities, helpers, and infrastructure.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages