Getting Started: Benzene on ASP.NET Core
This guide takes you from an empty folder to a running Benzene service on ASP.NET Core in about five minutes — no cloud account required. Reach for this host when you're building a plain web app or API, or running in a container that speaks HTTP. Deploying to Lambda or Functions instead? Start with AWS Lambda or Azure Functions — the handler you write is identical, only the host wiring differs.
Runnable version: this guide is
examples/Asp/Benzene.Example.Asp.Minimal— the smallest thing that works, ready todotnet run.
What you'll build
A single endpoint, GET /hello/{name}, that returns a JSON greeting. It's deliberately tiny so the focus
stays on the moving parts you'll reuse in every Benzene service: a message handler, a topic, and
the middleware pipeline that connects a transport to your handler.
Prerequisites
- .NET 10 SDK
- Any editor (Visual Studio, Rider, or VS Code)
The core idea in 60 seconds
Benzene separates what your service does from how it's invoked:
- A message handler contains your logic. It receives a typed request and returns a typed response wrapped in a result. It knows nothing about HTTP, Lambda, or queues.
- Each handler is mapped to a topic — a stable string like
hello:worldthat identifies the operation. Handlers are discovered automatically by reflection, so there's no routing table to maintain. - A transport (ASP.NET Core here; AWS Lambda or Azure Functions elsewhere) is wired up in a middleware pipeline that turns an incoming request into a message, routes it to the matching handler by topic, and turns the result back into a transport-native response.
Because only the transport pipeline changes between hosts, the handler you write below runs unchanged on every platform Benzene supports. See Message Handlers and Middleware for the full picture.
1. Create the project
mkdir HelloBenzene && cd HelloBenzene
dotnet new web -f net10.0
dotnet new web gives you a minimal ASP.NET Core app — a Program.cs and a .csproj, with no controllers
or extra scaffolding to clear away.
2. Install the Benzene package
Benzene's packages are published as prerelease (-alpha) versions until 1.0, so --prerelease is required:
dotnet add package Benzene.AspNet.Core --prerelease
Benzene.AspNet.Core brings in the middleware pipeline, the message handler infrastructure, and the
ASP.NET Core HTTP integration transitively — it's the only package you need for this guide.
3. Write a message handler
Create HelloWorldMessageHandler.cs. This is where your logic lives — and the only file you'd carry over
verbatim if you later moved to Lambda or Azure Functions:
using Benzene.Abstractions.MessageHandlers;
using Benzene.Abstractions.Results;
using Benzene.Core.MessageHandlers;
using Benzene.Http;
using Benzene.Results;
namespace HelloBenzene;
[Message("hello:world")]
[HttpEndpoint("GET", "/hello/{name}")]
public class HelloWorldMessageHandler : IMessageHandler<HelloWorldRequest, HelloWorldResponse>
{
public Task<IBenzeneResult<HelloWorldResponse>> HandleAsync(HelloWorldRequest message)
{
var response = new HelloWorldResponse { Message = $"Hello {message.Name}!" };
return Task.FromResult(BenzeneResult.Ok(response));
}
}
public class HelloWorldRequest
{
public string Name { get; set; }
}
public class HelloWorldResponse
{
public string Message { get; set; }
}
Two attributes do the wiring, and both are found by reflection:
[Message("hello:world")]maps the handler to its topic. Every Benzene transport routes by topic, so this is the identifier that stays constant across HTTP, Lambda, SQS, and the rest.[HttpEndpoint("GET", "/hello/{name}")]maps an HTTP method and path onto that same topic. The{name}segment is bound onto the request'sNameproperty.
The return type — Task<IBenzeneResult<HelloWorldResponse>> — is the response wrapped in a
result, which carries success/failure status alongside the payload.
BenzeneResult.Ok(...) is the success case.
4. Register and wire up Benzene
Replace the generated Program.cs with this:
using Benzene.AspNet.Core;
using Benzene.Core.MessageHandlers;
using Benzene.Core.MessageHandlers.DI;
using Benzene.Microsoft.Dependencies;
using HelloBenzene;
var builder = WebApplication.CreateBuilder(args);
// Register Benzene and discover message handlers in this assembly.
builder.Services.UsingBenzene(x => x
.AddMessageHandlers(typeof(HelloWorldMessageHandler).Assembly));
var app = builder.Build();
// Add the Benzene HTTP pipeline: turn each request into a message and route it to a handler.
app.UseBenzene(benzene => benzene
.UseHttp(http => http
.UseMessageHandlers()));
app.Run();
There are only two Benzene calls:
UsingBenzene(x => x.AddMessageHandlers(...))registers your message handlers and, because the router depends on them, pulls in the core pipeline services (routing, result statuses, serialization) for you — so there's no separateAddBenzene()to remember. Pass any type from the assembly you want scanned —typeof(HelloWorldMessageHandler).Assemblyhere. This registration has to happen here in the services phase (beforebuilder.Build()), because ASP.NET Core builds the service provider up front — unlike the self-hosted Benzene hosts (AWS Lambda, workers), where the pipeline you configure can register handlers implicitly.app.UseBenzene(benzene => benzene.UseHttp(http => http.UseMessageHandlers()))inserts Benzene into the ASP.NET Core request pipeline.UseHttpregisters the HTTP-specific services for you;UseMessageHandlers()is the step that routes a matched request to its handler.
The Benzene middleware only responds to requests that match one of your [HttpEndpoint] routes — anything
it doesn't recognise falls through to the rest of the ASP.NET Core pipeline, so it coexists cleanly with
controllers, static files, or health-check endpoints if you add them later.
5. Run it
dotnet run
The console prints the local URL (typically http://localhost:5000). In another terminal:
curl http://localhost:5000/hello/world
{"message":"Hello world!"}
That's a complete Benzene service. The request arrived over HTTP, Benzene mapped GET /hello/{name} to the
hello:world topic, bound world onto HelloWorldRequest.Name, invoked your handler, and serialised the
result back as JSON.
What just happened
GET /hello/world
│
▼
[HttpEndpoint] route match ──► topic "hello:world"
│
▼
HelloWorldMessageHandler.HandleAsync(request)
│
▼
BenzeneResult.Ok(response) ──► 200 {"message":"Hello world!"}
The handler in the middle never touched HttpContext. Swap the transport pipeline in Program.cs for an
AWS Lambda or Azure Functions one and the same handler runs there — that's the portability Benzene's
hexagonal design buys you.
Why not just a minimal API?
Worth asking honestly: app.MapGet("/hello/{name}", (string name) => new { message = $"Hello {name}!" })
is one line of plain ASP.NET Core, no package, no attributes. For an HTTP-only service that never talks
to anything else, that line does the same job this guide's five steps do, and you don't need Benzene to
get it — ASP.NET Core's own routing and middleware pipeline already give HTTP everything Benzene's
pipeline gives it here.
The payoff shows up the moment this same logic needs a second entry point — a queue another team
publishes to, a Kafka topic, a batch job that used to call this endpoint but really just wants to drop a
message. MapGet has no answer for that; you'd write a second, separate handler and keep both in sync by
hand. With Benzene the handler above doesn't change at all: Benzene.Aws.Sqs
or Benzene.Kafka.Core point a worker at the same
HelloWorldMessageHandler, because it was never written against HttpContext in the first place — see
examples/K8sTransports for that running, all three transports hosted
together in one container. (And once ASP.NET Core is only the HTTP host — no controllers, no other
ASP.NET middleware — UseAspNet hosts Kestrel as a worker inside a single Worker-platform startup,
lighter than this guide's embedded shape: see
Getting Started: Kubernetes.) If HTTP genuinely is and always will be
the only way in, reach for MapGet/MapPost/controllers instead — you'll write less code, not more.
Next steps
- Add validation — reject bad requests before they reach your handler with FluentValidation or Data Annotations.
- Add correlation & logging — trace requests end-to-end with Correlation IDs and Monitoring & Diagnostics.
- Understand the pipeline — see what else you can compose in with Middleware and Common Middleware.
- Test your handlers — Testing Benzene shows how to test handlers in isolation and pipelines end-to-end.
- Move to the cloud — the same handler runs on AWS Lambda (API Gateway, SQS, SNS, EventBridge) or Azure Functions.
- Go deeper with recipes — the Cookbooks cover retries, fan-out, caching, and distributed tracing.