Substratum.Generator
1.0.0-beta.46
See the version list below for details.
dotnet add package Substratum.Generator --version 1.0.0-beta.46
NuGet\Install-Package Substratum.Generator -Version 1.0.0-beta.46
<PackageReference Include="Substratum.Generator" Version="1.0.0-beta.46"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="Substratum.Generator" Version="1.0.0-beta.46" />
<PackageReference Include="Substratum.Generator"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add Substratum.Generator --version 1.0.0-beta.46
#r "nuget: Substratum.Generator, 1.0.0-beta.46"
#:package Substratum.Generator@1.0.0-beta.46
#addin nuget:?package=Substratum.Generator&version=1.0.0-beta.46&prerelease
#tool nuget:?package=Substratum.Generator&version=1.0.0-beta.46&prerelease
Substratum
Substratum is an opinionated, production-grade application framework built on ASP.NET Core and FastEndpoints. It eliminates the boilerplate required to bootstrap a modern web API — authentication, authorization, database, caching, logging, OpenAPI docs, cloud storage, and push notifications are all pre-wired and ready to go.
Write your business logic. Substratum handles the rest.
Packages
| Package | Description | Target |
|---|---|---|
| Substratum | Core runtime library — middleware, auth, EF Core, cloud integrations | net10.0 |
| Substratum.Generator | Roslyn source generators — permissions, reflection, service registration, endpoint summaries, document groups | netstandard2.0 |
| Substratum.Tools | CLI tool (dotnet-sub) — scaffold projects, endpoints, entities, and manage migrations |
net10.0 |
Quick Start
1. Install the CLI
dotnet tool install -g Substratum.Tools
2. Create a new project
dotnet-sub new webapp --name MyApp
This scaffolds a complete project with authentication, permissions, EF Core, localization, and health checks — ready to run.
3. Add packages to an existing project
<PackageReference Include="Substratum" Version="1.0.0-beta.46" />
<PackageReference Include="Substratum.Generator" Version="1.0.0-beta.46" />
4. Run the app
await SubstratumApp.RunAsync<AppDbContext, AppPermissions>(args, options =>
{
options.EntityFramework.Provider = EntityFrameworkProviders.Npgsql;
options.EntityFramework.ConnectionString = "Host=localhost;Database=myapp;...";
options.Authentication.JwtBearer.Enabled = true;
options.Authentication.JwtBearer.Options = new JwtBearerOptions
{
SecretKey = "your-256-bit-secret-key",
Issuer = "https://myapp.com",
Audience = "myapp-api"
};
});
Substratum (Core Library)
Endpoints
All endpoints inherit from BaseEndpoint<TRequest, TResponse> and return a standardized Result<TResponse>.
public class GetOrderEndpoint : BaseEndpoint<GetOrderRequest, OrderResponse>
{
public override void Configure()
{
Get("/orders/{id}");
PermissionsAny(AppPermissions.OrdersGet);
}
public override async Task<Result<OrderResponse>> ExecuteAsync(GetOrderRequest req, CancellationToken ct)
{
var order = await db.Orders.FindAsync(req.Id, ct);
if (order is null)
return Failure(404, "OrderNotFound");
return Success("OrderRetrieved", new OrderResponse { Id = order.Id, Total = order.Total });
}
}
Result<T> wraps every response:
| Property | Type | Description |
|---|---|---|
Code |
int |
0 for success, 1 for failure |
Message |
string |
Human-readable (localized) message |
Data |
T? |
Response payload |
Errors |
IReadOnlyList<string>? |
Validation or error details |
For endpoints that return no data, use Unit as the response type.
Pagination
public class ListOrdersEndpoint : BaseEndpoint<ListOrdersRequest, PaginatedResult<OrderResponse>>
{
public override async Task<Result<PaginatedResult<OrderResponse>>> ExecuteAsync(
ListOrdersRequest req, CancellationToken ct)
{
var result = await PaginatedResult<OrderResponse>.CreateAsync(
db.Orders.Select(o => new OrderResponse { Id = o.Id, Total = o.Total }),
req.PageNumber,
req.PageSize,
ct
);
return Success("OrdersRetrieved", result);
}
}
PaginatedResult<T> includes PageNumber, TotalPages, TotalCount, Items, HasPreviousPage, and HasNextPage.
Base Entity
All domain entities inherit from BaseEntity<T> which provides built-in audit fields:
public sealed class Order : BaseEntity<Guid>
{
public decimal Total { get; set; }
public string Status { get; set; }
}
| Property | Type | Description |
|---|---|---|
Id |
T |
Primary key |
CreatedAt |
DateTimeOffset |
Auto-set on creation |
UpdatedAt |
DateTimeOffset |
Auto-set on update |
IsDeleted |
bool |
Soft delete flag |
DeletedAt |
DateTimeOffset? |
Soft delete timestamp |
Authentication
Substratum supports four authentication schemes out of the box. Enable any combination — the framework automatically configures a policy scheme that routes to the correct handler.
JWT Bearer
options.Authentication.JwtBearer.Enabled = true;
options.Authentication.JwtBearer.Options = new JwtBearerOptions
{
SecretKey = "your-256-bit-secret-key",
Issuer = "https://myapp.com",
Audience = "myapp-api",
Expiration = TimeSpan.FromHours(1)
};
Create tokens with IJwtBearer:
var (accessToken, sessionId, expiration) = jwtBearer.CreateToken(userId);
Cookie
options.Authentication.Cookie.Enabled = true;
options.Authentication.Cookie.Options = new CookieOptions
{
CookieName = "auth_token",
Expiration = TimeSpan.FromHours(1),
SlidingExpiration = true,
SameSite = SameSiteMode.Strict
};
Sign in/out with ICookieAuth:
var (sessionId, expiration) = await cookieAuth.SignInAsync(HttpContext, userId, ct);
await cookieAuth.SignOutAsync(HttpContext, ct);
Basic Authentication
options.Authentication.BasicAuthentication.Enabled = true;
options.Authentication.BasicAuthentication.Options = new BasicAuthenticationOptions
{
Realm = "MyAPI"
};
Implement IBasicAuthValidator:
public class BasicAuthValidator : IBasicAuthValidator
{
public async Task<(bool Result, string UserId, string SessionId)> ValidateAsync(
HttpContext context, string username, string password, CancellationToken ct)
{
var user = await db.Users.FirstOrDefaultAsync(u => u.Email == username, ct);
if (user is null || !passwordHasher.VerifyHashedPassword(user.PasswordHash, password, out _))
return (false, "", "");
return (true, user.Id.ToString("N"), Guid.CreateVersion7().ToString("N"));
}
}
API Key
options.Authentication.ApiKeyAuthentication.Enabled = true;
options.Authentication.ApiKeyAuthentication.Options = new ApiKeyAuthenticationOptions
{
KeyName = "X-API-KEY",
Realm = "MyAPI"
};
Implement IApiKeyValidator:
public class ApiKeyValidator : IApiKeyValidator
{
public async Task<(bool Result, string UserId, string SessionId)> ValidateAsync(
HttpContext context, string apiKey, CancellationToken ct)
{
var key = await db.ApiKeys.FirstOrDefaultAsync(k => k.Key == apiKey && k.IsActive, ct);
if (key is null) return (false, "", "");
return (true, key.UserId.ToString("N"), Guid.CreateVersion7().ToString("N"));
}
}
Supporting Interfaces
| Interface | Purpose | Required By |
|---|---|---|
ISessionValidator |
Validates active sessions server-side | JWT, Cookie |
IPermissionHydrator |
Loads user permissions into claims | All schemes |
IPasswordHasher |
Hashes and verifies passwords (PBKDF2, 600k iterations) | Built-in service |
ICurrentUser |
Access current user's ID (Guid?) |
Built-in service |
Permissions
Define permissions as static fields in a partial class implementing IPermissionRegistry. The source generator auto-generates Definitions(), Parse(), and TryParse() methods.
public partial class AppPermissions : IPermissionRegistry
{
public static readonly PermissionDefinition OrdersGet;
public static readonly PermissionDefinition OrdersCreate;
public static readonly PermissionDefinition OrdersEdit;
public static readonly PermissionDefinition OrdersDelete;
public static readonly PermissionDefinition UsersGet;
public static readonly PermissionDefinition UsersCreate;
}
The generator produces:
- Code: unique short code derived from the field name (e.g.
A1B2) - Name: snake_case from the field name (e.g.
orders_get) - DisplayName: human-readable (e.g.
Get) - Group: inferred from the field name prefix (e.g.
Ordersgroup)
Use permissions in endpoints:
public override void Configure()
{
Get("/orders/{id}");
PermissionsAny(AppPermissions.OrdersGet); // user needs ANY of these
PermissionsAll(AppPermissions.OrdersGet); // user needs ALL of these
}
Implement IPermissionHydrator to load permissions from your database into claims:
public class PermissionHydrator : IPermissionHydrator
{
public async Task HydrateAsync(IServiceProvider sp, ClaimsPrincipal principal, CancellationToken ct)
{
var userId = principal.FindFirstValue(SubstratumClaimsTypes.UserId);
var db = sp.GetRequiredService<AppDbContext>();
var codes = await db.UserPermissions
.Where(up => up.UserId == Guid.Parse(userId))
.Select(up => up.Permission.Code)
.ToListAsync(ct);
var identity = (ClaimsIdentity)principal.Identity!;
foreach (var code in codes)
identity.AddClaim(new Claim("permissions", code));
}
}
Document Groups
Organize your API into separate OpenAPI documents — each with its own Scalar UI page and optional authentication.
public partial class AppDocumentGroups : IDocumentGroupRegistry
{
public static readonly DocumentGroupDefinition Mobile =
new("Mobile API", "mobile");
public static readonly DocumentGroupDefinition Admin =
new("Admin API", "admin", AppPermissions.AdminDocsAccess); // protected
}
Use in endpoints:
public override void Configure()
{
Get("/orders");
DocumentGroup(AppDocumentGroups.Mobile);
}
This creates:
/mobile— Scalar UI showing only Mobile API endpoints/admin— Scalar UI showing only Admin API endpoints (requires API key)/openapi/mobile.jsonand/openapi/admin.json— separate OpenAPI schemas
Protected groups use Basic Auth where the username is the API key and the password is empty. The browser's native auth dialog prompts the user. Permissions are validated through IApiKeyValidator and IPermissionHydrator.
Entity Framework
options.EntityFramework.Provider = EntityFrameworkProviders.Npgsql; // or SqlServer, Sqlite
options.EntityFramework.ConnectionString = "Host=localhost;Database=myapp;...";
Supported providers: PostgreSQL, SQL Server, SQLite
Retry Policy
options.EntityFramework.RetryPolicy.Enabled = true;
options.EntityFramework.RetryPolicy.Options = new RetryPolicyOptions
{
MaxRetryCount = 3,
MaxRetryDelaySeconds = 2
};
Second-Level Cache
options.EntityFramework.SecondLevelCache.Enabled = true;
options.EntityFramework.SecondLevelCache.Options = new SecondLevelCacheOptions
{
Provider = SecondLevelCacheProviders.Memory, // or Redis
KeyPrefix = "app:"
};
// Redis configuration
options.EntityFramework.SecondLevelCache.Options.Redis = new SecondLevelCacheRedisOptions
{
ConnectionString = "localhost:6379",
TimeoutSeconds = 300
};
OpenAPI Documentation
options.OpenApi.Enabled = true;
options.OpenApi.Options.Servers = new[]
{
new OpenApiServerOptions { Url = "https://api.example.com", Description = "Production" },
new OpenApiServerOptions { Url = "https://staging.example.com", Description = "Staging" }
};
Substratum generates Scalar UI at /docs (or at document group URLs). Features include:
- Automatic permission documentation on each endpoint
- Accept-Language header parameter for localized APIs
- Dark mode theme
Localization
options.Localization.DefaultCulture = "en";
options.Localization.SupportedCultures = ["en", "ar", "fr"];
options.Localization.ResourceSource = typeof(SharedResource);
The framework reads Accept-Language headers and localizes validation messages, error responses, and OpenAPI descriptions.
Cloud Storage
MinIO (S3-Compatible)
options.Minio.Enabled = true;
options.Minio.Options = new MinioOptions
{
Endpoint = "minio.example.com",
Region = "us-east-1",
Secure = true,
AccessKey = "...",
SecretKey = "..."
};
Inject IMinioClient to interact with S3-compatible storage.
AWS S3
options.Aws.S3.Enabled = true;
options.Aws.S3.Options = new AwsS3Options
{
Region = "us-west-2",
AccessKey = "...",
SecretKey = "..."
};
Inject IAmazonS3 to interact with AWS S3.
AWS Secrets Manager
options.Aws.SecretsManager.Enabled = true;
options.Aws.SecretsManager.Options = new AwsSecretsManagerOptions
{
Region = "us-west-2",
AccessKey = "...",
SecretKey = "...",
SecretArns = ["arn:aws:secretsmanager:us-west-2:123456789:secret:my-secret"]
};
Secrets are automatically loaded into IConfiguration.
Firebase
Cloud Messaging
options.Firebase.Messaging.Enabled = true;
options.Firebase.Messaging.Options = new FirebaseMessagingOptions
{
Credential = Convert.ToBase64String(Encoding.UTF8.GetBytes(serviceAccountJson))
};
Inject FirebaseMessaging to send push notifications.
App Check
options.Firebase.AppCheck.Enabled = true;
options.Firebase.AppCheck.Options = new FirebaseAppCheckOptions
{
ProjectId = "my-project",
ProjectNumber = "123456789"
};
Inject IFirebaseAppCheck and call VerifyAppCheckTokenAsync().
Infrastructure
| Feature | Configuration | Default |
|---|---|---|
| CORS | options.Cors.AllowedOrigins = [...] |
Disabled |
| Health Checks | options.HealthChecks.Enabled = true |
/healthz |
| Static Files | options.StaticFiles.Enabled = true |
wwwroot |
| Response Compression | Always on | Gzip + Brotli |
| Forwarded Headers | Always on | X-Forwarded-For/Proto |
| Logging | Serilog via appsettings.json |
Console sink, sensitive data masking |
| Kestrel Limits | Pre-configured | 1 MB body, 10k connections, 15s header timeout |
Substratum.Generator
Seven Roslyn incremental source generators that eliminate boilerplate at compile time.
1. SubstratumApp Generator
Generates a [ModuleInitializer] that wires up the framework. Scans for:
DbContextimplementationIPermissionRegistryimplementationISessionValidator,IPermissionHydrator,IBasicAuthValidator,IApiKeyValidatorimplementations
Output: SubstratumAppInitializer.g.cs
2. Discovered Types Generator
Scans for all concrete classes implementing FastEndpoints interfaces (IEndpoint, IEventHandler, ICommandHandler, ISummary, IValidator) and collects them into a type list. Respects [DontRegister].
Output: DiscoveredTypes.g.cs
3. Service Registration Generator
Scans for [RegisterService<TInterface>(ServiceLifetime)] attributes and generates DI extension methods.
[RegisterService<IOrderService>(ServiceLifetime.Scoped)]
public class OrderService : IOrderService { }
Output: ServiceRegistrations.g.cs with RegisterServicesFromMyApp() extension method.
4. Reflection Generator
Builds a FastEndpoints reflection cache by analyzing endpoint request/response DTOs — object factories, property setters, and value parsers. Eliminates runtime reflection.
Output: ReflectionData.g.cs
5. Permissions Generator
Generates the Definitions(), Parse(), and TryParse() methods for IPermissionRegistry implementations. Auto-computes permission codes, names, display names, and groups from field names.
Output: {ClassName}.Permissions.g.cs
6. Endpoint Summary Generator
Scans BaseEndpoint<TRequest, TResponse> subclasses and generates OpenAPI summary classes. Auto-detects:
- Error responses from
Failure()calls - 400 if a
Validator<TRequest>exists - 401 if the endpoint is not
AllowAnonymous() - 403 if permissions are declared
Output: {EndpointName}Summary.g.cs
7. Document Group Generator
Generates Definitions() and a [ModuleInitializer] for IDocumentGroupRegistry implementations.
Output: {ClassName}.DocumentGroups.g.cs
Substratum.Tools (CLI)
Install globally:
dotnet tool install -g Substratum.Tools
Commands
dotnet-sub new webapp
Scaffold a complete web application:
dotnet-sub new webapp --name MyApp
Generates a full project with:
- Pre-configured
Program.cswithSubstratumApp.RunAsync AppDbContextwith User, Role, UserSession entitiesAppPermissionsregistrySessionValidatorandPermissionHydrator- Localization resources (EN, AR)
- Initial EF migration
AGENTS.mdwith AI scaffolding guidelines
dotnet-sub new endpoint
Scaffold a new API endpoint:
dotnet-sub new endpoint \
--group Orders \
--name CreateOrder \
--route /orders \
--method Post \
--permission OrdersCreate \
--response-type SingleResult
Generates under Features/Orders/CreateOrder/:
CreateOrderEndpoint.cs— endpoint handlerCreateOrderRequest.cs— request DTOCreateOrderResponse.cs— response DTOCreateOrderRequestValidator.cs— FluentValidation validatorCreateOrderSummary.cs— OpenAPI summaryCreateOrderSerializerContext.cs— JSON source-gen context
Also inserts the permission definition into AppPermissions.cs automatically.
For paginated endpoints, use --response-type PaginatedResult — the request will include PageNumber and PageSize properties.
All options are interactive — omit any flag and the CLI will prompt you.
dotnet-sub new entity
Scaffold a domain entity:
dotnet-sub new entity --name Order
Generates:
Domain/Entities/Order.cs— entity class inheritingBaseEntity<Guid>Data/Configurations/OrderConfiguration.cs— EF Core configuration
Also inserts public DbSet<Order> Orders => Set<Order>(); into AppDbContext.cs.
dotnet-sub migrations add
Add an EF Core migration:
dotnet-sub migrations add AddOrderTable
dotnet-sub database update
Apply pending migrations:
dotnet-sub database update
dotnet-sub database sql
Generate an idempotent SQL script:
dotnet-sub database sql -o ./deploy.sql
Configuration Reference
await SubstratumApp.RunAsync<AppDbContext, AppPermissions>(args, options =>
{
// Authentication
options.Authentication.JwtBearer.Enabled = true;
options.Authentication.JwtBearer.Options = new JwtBearerOptions { ... };
options.Authentication.Cookie.Enabled = true;
options.Authentication.Cookie.Options = new CookieOptions { ... };
options.Authentication.BasicAuthentication.Enabled = true;
options.Authentication.BasicAuthentication.Options = new BasicAuthenticationOptions { ... };
options.Authentication.ApiKeyAuthentication.Enabled = true;
options.Authentication.ApiKeyAuthentication.Options = new ApiKeyAuthenticationOptions { ... };
// Database
options.EntityFramework.Provider = EntityFrameworkProviders.Npgsql;
options.EntityFramework.ConnectionString = "...";
options.EntityFramework.CommandTimeoutSeconds = 30;
options.EntityFramework.RetryPolicy.Enabled = true;
options.EntityFramework.RetryPolicy.Options = new RetryPolicyOptions { ... };
options.EntityFramework.SecondLevelCache.Enabled = true;
options.EntityFramework.SecondLevelCache.Options = new SecondLevelCacheOptions { ... };
// Localization
options.Localization.DefaultCulture = "en";
options.Localization.SupportedCultures = ["en", "ar"];
options.Localization.ResourceSource = typeof(SharedResource);
// OpenAPI
options.OpenApi.Enabled = true;
options.OpenApi.Options.Servers = [new OpenApiServerOptions { ... }];
// CORS
options.Cors.AllowedOrigins = ["https://example.com"];
// Health Checks
options.HealthChecks.Enabled = true;
options.HealthChecks.Options = new HealthChecksOptions
{
Path = "/healthz",
HealthChecksBuilder = hc => hc.AddDbContextCheck<AppDbContext>()
};
// Static Files
options.StaticFiles.Enabled = true;
options.StaticFiles.Options = new StaticFilesOptions { RootPath = "wwwroot" };
// Error Handling
options.ErrorHandling.IncludeExceptionDetails = false;
// Cloud — MinIO
options.Minio.Enabled = true;
options.Minio.Options = new MinioOptions { ... };
// Cloud — AWS
options.Aws.S3.Enabled = true;
options.Aws.S3.Options = new AwsS3Options { ... };
options.Aws.SecretsManager.Enabled = true;
options.Aws.SecretsManager.Options = new AwsSecretsManagerOptions { ... };
// Firebase
options.Firebase.Messaging.Enabled = true;
options.Firebase.Messaging.Options = new FirebaseMessagingOptions { ... };
options.Firebase.AppCheck.Enabled = true;
options.Firebase.AppCheck.Options = new FirebaseAppCheckOptions { ... };
// Custom Services
options.Services.AddScoped<IMyService, MyService>();
});
Interfaces to Implement
| Interface | Purpose | When Required |
|---|---|---|
IPermissionRegistry |
Define all permissions | Always (source-generated) |
IDocumentGroupRegistry |
Define API document groups | When using document groups (source-generated) |
ISessionValidator |
Validate active sessions | JWT or Cookie auth |
IPermissionHydrator |
Load user permissions into claims | When using permissions |
IBasicAuthValidator |
Validate username/password | Basic auth enabled |
IApiKeyValidator |
Validate API keys | API key auth or protected document groups |
All implementations are auto-discovered by the source generators — no manual registration needed.
License
See LICENSE for details.
Learn more about Target Frameworks and .NET Standard.
-
.NETStandard 2.0
- FastEndpoints.Attributes (>= 7.2.0)
- Scriban (>= 6.2.1)
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 |
|---|---|---|
| 1.0.0-beta.148 | 0 | 2/21/2026 |
| 1.0.0-beta.147 | 25 | 2/21/2026 |
| 1.0.0-beta.146 | 34 | 2/18/2026 |
| 1.0.0-beta.145 | 29 | 2/18/2026 |
| 1.0.0-beta.144 | 28 | 2/18/2026 |
| 1.0.0-beta.143 | 32 | 2/18/2026 |
| 1.0.0-beta.141 | 29 | 2/17/2026 |
| 1.0.0-beta.140 | 30 | 2/17/2026 |
| 1.0.0-beta.130 | 34 | 2/17/2026 |
| 1.0.0-beta.122 | 37 | 2/16/2026 |
| 1.0.0-beta.110 | 37 | 2/15/2026 |
| 1.0.0-beta.107 | 37 | 2/15/2026 |
| 1.0.0-beta.104 | 38 | 2/14/2026 |
| 1.0.0-beta.103 | 41 | 2/14/2026 |
| 1.0.0-beta.102 | 37 | 2/14/2026 |
| 1.0.0-beta.100 | 36 | 2/14/2026 |
| 1.0.0-beta.96 | 39 | 2/14/2026 |
| 1.0.0-beta.94 | 36 | 2/14/2026 |
| 1.0.0-beta.90 | 36 | 2/14/2026 |
| 1.0.0-beta.46 | 41 | 2/7/2026 |