A comprehensive framework for building resilient, data-driven C# applications with standardized patterns for data access, HTTP communication, messaging, and API integration.
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.
- 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
.kdbxreader 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
┌────────────────────────────────────────────────────────────────────────────┐
│ 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, andRoadbed.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) andRoadbed.IO.Csv(CSV reading/writing). Roadbed.CrudandRoadbed.Loggingare the only consumable-layer packages that sit onRoadbed.Data\*; the rest reachRoadbed.Commondirectly.
For detailed documentation on each package:
- Roadbed.Common - Shared types and utilities
- Roadbed.Crud - Repository/Entity patterns
- Roadbed.Data - Data access abstractions
- Roadbed.Data.Dapper - Dapper configuration
- Roadbed.Data.MySql - MySQL data access
- Roadbed.Data.Sqlite - SQLite data access
- Roadbed.IO - File I/O and CSV operations
- Roadbed.Logging - OpenTelemetry-backed MEL-to-database logging and activity/run-instance/lineage tracking
- Roadbed.Messaging - Message envelopes
- Roadbed.Net - HTTP client wrapper
- Roadbed.Scheduling - Job scheduling
- Roadbed.Secrets.KeePass - KeePass-backed secret loading
dotnet add package Roadbed.Commonusing Roadbed.Crud;
public sealed record FooRecord : BaseEntityRecord<long>
{
public string? Name { get; set; }
public string? Description { get; set; }
}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();
}
}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);
}
}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());
}
}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();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;
}
}Roadbed uses IServiceCollectionInstaller to automatically discover and register services across assemblies, eliminating manual wiring in Program.cs.
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);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.
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.
NetHttpResponse<T> provides consistent success/failure patterns across HTTP boundaries with built-in retry and backoff support via NetHttpRequest.
While generally an anti-pattern, ServiceLocator enables NuGet packages to operate self-contained without requiring consumers to manually register internal dependencies.
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;
}
}- .NET 10.0+
- Microsoft.Extensions.DependencyInjection
- Microsoft.Extensions.Logging
- Microsoft.Extensions.Configuration