Skip to content
Alvo is pre-v0.1: the image runs from its edge tag, and no NuGet package or release is published yet.Pre-v0.1: no release yet.Roadmap and status

Embed Alvo in ASP.NET Core

Add Alvo to an existing ASP.NET Core app, so it serves its API next to your own endpoints from the same descriptor.

  • The .NET 10 SDK and an ASP.NET Core project (dotnet new web is enough), plus a clone of the repository: Alvo is pre-v0.1 and no package is on NuGet yet.
  • A descriptor. The code below uses examples/vehicle-registry; Run your own descriptor is the Docker-only way to try one first.

Two packages are the minimum: MMLib.Alvo, the core (schema registry, Data API, rule engine, events, management), and one database driver, MMLib.Alvo.Data.Sqlite or MMLib.Alvo.Data.PostgreSql. The core never references a driver; the driver you add is the one you select in code. Every package is listed in the C# API reference.

Today you either reference the projects in a clone or install from a local package feed you pack yourself; dotnet add package from NuGet works from v0.1.

Reference the projects in your clone directly. ALVO_CLONE is where you cloned the repository:

Terminal (in your project)
CLONE="${ALVO_CLONE:-$HOME/MMLib.Alvo}"
dotnet add reference "$CLONE/src/MMLib.Alvo/MMLib.Alvo.csproj"
dotnet add reference "$CLONE/src/MMLib.Alvo.Data.Sqlite/MMLib.Alvo.Data.Sqlite.csproj"

The whole app fits in one Program.cs. AddAlvo is the one entry point: UseSqlite selects the database, FromDescriptor supplies the descriptor (here vehicles.alvo.json, copied beside the project), and AddDataApi mounts the generated API under /api/alvo so it never collides with your own routes:

Program.cs
using MMLib.Alvo.Auth;
var builder = WebApplication.CreateBuilder(args);
// Alvo's own API keys, from configuration (Alvo:Auth).
builder.Services.Configure<AlvoAuthOptions>(builder.Configuration.GetSection("Alvo:Auth"));
builder.Services.AddAlvo(alvo => alvo
.UseSqlite("Data Source=alvo.db")
.FromDescriptor("vehicles.alvo.json")
.AddDataApi(api => api.RoutePrefix = "/api/alvo"));
var app = builder.Build();
app.MapAlvoHealth();
app.MapAlvoDataApi();
app.Run();

There is no call to apply the descriptor: AddAlvo registers a hosted service that brings the schema up before the app starts serving, and /health/ready answers 200 once it has.

The API keys come from configuration, which is what the Configure<AlvoAuthOptions> line binds. Declare a dev key in appsettings.json, deliberately without a secret, as the repository’s embedded sample does:

appsettings.json
"Auth": {
"DevKeys": [
{
"KeyId": "agent",
"User": "9f1d3c7e-5b2a-4f18-8c6d-2e7a9b4c1d05",
"Roles": [ "admin", "authenticated" ],
"Scopes": [ "*:read", "*:write" ]
}
]
}

Alvo refuses to start without a secret of at least 32 characters, so the app cannot ship a working default credential. Supply it outside source control, with user secrets or an environment variable.

Mapping is a separate step, so the routes land exactly where your pipeline wants them. MapAlvoHealth maps /health/live and /health/ready on their own. MapAlvoDataApi returns a convention builder over Alvo’s generated data routes and nothing else, so you can attach your own rate limiting, telemetry, tags or an authorization policy to them, and none of it ever reaches the probes, which a container calls without a credential.

To try the pattern without writing a project, run the repository’s embedded sample: it serves the same descriptor with the same registration, and adds a few endpoints of its own. Give its dev key a secret, then start it from the clone:

Terminal (in the clone)
dotnet user-secrets --project samples/MMLib.Alvo.Samples.EmbeddedHost \
set "Alvo:Auth:DevKeys:0:Secret" "$(openssl rand -hex 16)"
dotnet run --project samples/MMLib.Alvo.Samples.EmbeddedHost

It listens on http://localhost:5199 and keeps a SQLite file beside itself. The sample’s README shows a request to each of its two surfaces under Try both.

The admin dashboard and the schema assistant are three more packages: MMLib.Alvo.Admin (the dashboard, a server-interactive Blazor app), MMLib.Alvo.Identity (local accounts and the cookie sign-in) and MMLib.Alvo.Ai (the assistant). The same Program.cs with the dashboard added:

Program.cs
using Microsoft.EntityFrameworkCore;
using MMLib.Alvo.Admin;
using MMLib.Alvo.Auth;
using MMLib.Alvo.Identity;
using System.Security.Claims;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<AlvoAuthOptions>(builder.Configuration.GetSection("Alvo:Auth"));
builder.Services.AddAlvo(alvo => alvo
.UseSqlite("Data Source=alvo.db")
.FromDescriptor("vehicles.alvo.json")
.AddDataApi(api => api.RoutePrefix = "/api/alvo"));
// The dashboard: local accounts and their cookie sign-in, the dashboard itself, and the schema assistant.
builder.Services.AddAlvoIdentity(
store => store.UseSqlite("Data Source=alvo-identity.db"),
identity => builder.Configuration.GetSection(AlvoIdentity.ConfigurationSection).Bind(identity));
builder.Services.AddAlvoIdentityCookieSignIn(AlvoAdmin.SignInPath);
builder.Services.AddAlvoAdmin(admin => builder.Configuration.GetSection(AlvoAdmin.ConfigurationSection).Bind(admin));
builder.Services.AddAlvoAi();
builder.Services.AddScoped<IAlvoAdminCallerResolver, AdminCallerResolver>();
var app = builder.Build();
app.MapStaticAssets();
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
app.MapAlvoHealth();
app.MapAlvoDataApi();
app.MapAlvoAdmin();
app.Run();
// The one port between the dashboard and the identity package: the signed-in operator becomes the caller Alvo judges.
sealed class AdminCallerResolver(IServiceProvider services) : IAlvoAdminCallerResolver
{
public ValueTask<AlvoPrincipal?> ResolveAsync(ClaimsPrincipal signedIn, CancellationToken cancellationToken)
{
var subject = signedIn.FindFirstValue(ClaimTypes.NameIdentifier);
if (subject is not { Length: > 0 })
{
return ValueTask.FromResult<AlvoPrincipal?>(null);
}
var resolver = services.GetRequiredKeyedService<IAlvoContextResolver>(AlvoIdentity.ResolverKey);
return resolver.ResolveAsync(subject, requestedTenant: null, cancellationToken);
}
}

The dashboard references neither the identity package nor the core, so the host fills the one port between them, IAlvoAdminCallerResolver, which turns the signed-in operator into the caller Alvo authorizes; over MMLib.Alvo.Identity it asks the identity package’s own resolver, registered under AlvoIdentity.ResolverKey. MapAlvoAdmin maps the dashboard’s components and nothing else; the middleware before it is yours to order. The standalone image’s AlvoHost.cs is the complete composition.

Two pieces stay with the host. The sign-in, sign-out and set-password screens post to AlvoAdmin.SignInEndpoint, AlvoAdmin.SignOutEndpoint and AlvoAdmin.SetPasswordEndpoint, and no package maps those: map them over AlvoSignIn, validating the antiforgery token in each, as the standalone host’s AlvoAdminSignIn.cs does. A host with its own users skips MMLib.Alvo.Identity and implements IAlvoAdminCallerResolver over its own store instead. The packages’ entry points are in the C# reference: MMLib.Alvo.Admin and MMLib.Alvo.Identity.

Alvo adds no authentication, authorization or routing middleware on your behalf. Three things stay yours:

  • Authentication of your own users. Alvo’s generated API authenticates its own API keys. Your app’s cookie or token users reach the same data through your own endpoints, which pass Alvo the caller and let the descriptor’s rules decide: Use your own authentication.
  • Error rendering. The code above deliberately does not call AddAlvoProblemDetails(), because an embedded host owns the shape of its error responses. Without it, Alvo’s endpoints still answer their own refusals as problem documents, but three problem types are only produced with it: unreadable-request, internal and function-failed (Problem types).
  • Your own endpoints. They call Alvo in process through IAlvoData, with no HTTP round trip and no second authorization model: Call Alvo from your endpoints.
StatusProblem typeWhenFixReturned by
401unauthenticatedAn API key was sent to /api/alvo and cannot be used: a wrong secret, or a role the descriptor does not declare.Use the secret you configured; give the key only roles in auth.roles or built in.every host

If the app stops at startup instead, the message names the option it refused, such as a dev key with no secret, and what to set.

Call Alvo from your endpoints: read and write Alvo’s data from your own minimal-API routes, under your users’ identity and the descriptor’s rules.