Project templates
dotnet new hardened-web writes a solution that builds, tests and serves. Two more templates write a Lambda function and a reusable library.
dotnet new install Hardened.Templates
dotnet new hardened-web -n Todos
cd Todos
dotnet run --project src/Todos.Host$ curl localhost:5080/todos
[{"id":1,"title":"Read the generated code","done":true},{"id":2,"title":"Add an endpoint","done":false}]The reference page is at http://localhost:5080/docs.
| Short name | Writes |
|---|---|
hardened-web | An HTTP API on Kestrel, ASP.NET Core or AWS Lambda |
hardened-function | An AWS Lambda function that is not an HTTP API |
hardened-library | A module other applications compose |
hardened-web
dotnet new hardened-web -n Todos [options]| Option | Values | Default |
|---|---|---|
-ho, --host | kestrel, aspnet, aws-lambda | kestrel |
-c, --contract | code, openapi, smithy | code |
-rm, --response-model | response, throws, union | response |
-cl, --client | kiota, refit, none | kiota |
--test-framework | xunit, nunit | xunit |
--mocks | nsubstitute, moq, fakeiteasy | nsubstitute |
--openapi-ui | true, false | true |
--hardened-version | a published version | the version the template shipped with |
--skip-restore | true, false | false |
What you get
Todos.sln
.config/dotnet-tools.json the client generator, Kiota or Refitter
Directory.Packages.props every version, in one place
src/Todos/ the implementation. Knows nothing about where it runs
src/Todos/openapi/Todos.json the served document, written by the build and committed
src/Todos.Host/ the runtime, and Program.cs
src/Todos.Client/ the generated client. No hand-written code
tests/Todos.Tests/ tests against the library, not the hostSwapping --host changes only the host project. The library, the client and the tests are the same whichever host you pick.
The host
kestrel serves HTTP through Kestrel without the ASP.NET Core request pipeline.
aspnet is for an application that needs ASP.NET Core's own middleware, authentication and authorization, or its hosting diagnostics. Instrumentation that subscribes to the ASP.NET DiagnosticSource names sees nothing under Kestrel. The Kestrel host's README lists the trade-offs.
aws-lambda puts the application behind API Gateway. The host project has a Program.cs like every other host, and the call to LambdaEmulator.StartIfLocal in it is what starts the AWS Lambda Test Tool beside the process when it is not the Lambda service running it. The application answers on 5080 through the tool's API Gateway emulator, so dotnet run --project src/Todos.Host and F5 work the way they do on the other hosts; see Running it locally.
The contract
code: the C# is the contract. Routes are attributes on methods, and the OpenAPI document is generated from them.
openapi: an OpenAPI document is the contract. The models, the service interface, the routes and the validation are generated from it.
smithy: the same, from a Smithy model. The build runs the Smithy CLI and names the version it expects if yours differs.
With openapi or smithy there are no route attributes in the project. Add an operation to the contract and the build fails until the service implements it.
The response model
--response-model decides how a handler declares more than one kind of response. The scaffolded routes show the difference: they answer 404 and 409 in every mode, and creating a todo answers 201 under response and union and 200 under throws. Declared responses covers the three.
union writes a net11.0 project pinned to the .NET 11 SDK in global.json. It cannot be combined with --host aws-lambda, whose managed runtime is net8.0. dotnet new cannot refuse a combination of options, so the first build refuses with HTPL001.
standard is accepted as the old name for throws and writes the same project.
The client
kiota writes a Kiota client under src/Todos.Client, generated during the build from the document the library writes, and tests that drive it through the pipeline. refit writes a Refit interface with Refitter instead, and every operation on it returns IApiResponse<T>. none leaves out the client project and the tool manifest, and the same tests drive the pipeline through ITestWebApp. See Generated clients and Typed clients.
The reference page
--openapi-ui serves a page at /docs describing every operation, and the document behind it at /openapi.json. It is served in the development environment only. Name more:
[HardenedOpenApiUi(Title = "Todos", Environments = "development,staging")]The environment is HARDENED_ENVIRONMENT, which defaults to development. See Environments.
hardened-function
dotnet new hardened-function -n OrderIntake [options]| Option | Values | Default |
|---|---|---|
--trigger | invoke, queue, topic, timer, change, stream, blob | invoke |
-ho, --host | aws | aws |
--test-framework | xunit, nunit | xunit |
--mocks | nsubstitute, moq, fakeiteasy | nsubstitute |
--hardened-version | a published version | the version the template shipped with |
--skip-restore | true, false | false |
src/OrderIntake/ the function: its handler, models and services
src/OrderIntake/Program.cs the entry point, and the local emulator
tests/OrderIntake.Tests/ tests that invoke it the way Lambda doesThe handler is a plain class:
public class OrderHandler(OrderLog log) {
[HardenedFunction]
public Task<OrderAccepted> Process(Order order) { ... }
}There is no separate host project. The deployed artifact is this assembly, and Program.cs is the entry point the runtime starts — written rather than generated, so do not add a Main of your own.
--trigger picks which source the scaffolded handler serves, and with it the one adapter package the project references. Each is a trigger attribute naming the queue, topic, schedule, table, stream or bucket, and nothing in the project names a cloud:
--trigger | Handler carries | AWS adapter |
|---|---|---|
invoke | [HardenedFunction] | Hardened.Aws.Lambda.Invoke |
queue | [Queue("orders")] | Hardened.Aws.Lambda.Sqs |
topic | [Topic("orders")] | Hardened.Aws.Lambda.Sns |
timer | [Timer("nightly")] | Hardened.Aws.Lambda.EventBridge |
change | [Change("orders")] | Hardened.Aws.Lambda.DynamoDb |
stream | [Stream("orders")] | Hardened.Aws.Lambda.Kinesis |
blob | [Blob("uploads")] | Hardened.Aws.Lambda.S3 |
On a batched trigger the runtime unpacks the batch and calls the handler once per item. Returning handles the item; throwing fails the invocation, which is what returns the batch to the source. Reporting individual failures instead is a deployment setting the application has to opt into, and it has to match the event source mapping — see Batches.
Running the project starts the AWS Lambda Test Tool on 5050, which is where a payload is posted; there is no HTTP API and nothing on 5080. Most of the time there is nothing to run, because the tests invoke the function through the real pipeline with no AWS account and nothing to deploy. See Lambda functions.
hardened-library
dotnet new hardened-library -n Acme.GreetingA module that names no runtime, so one package serves Kestrel, ASP.NET Core and Lambda. The build writes an attribute named after the module, and an application composes it the way it composes a runtime:
[HardenedModule]
[KestrelRuntime]
[AcmeGreetingLibrary]
public partial class Application;There is no AddAcmeGreeting() to call and no options object to thread through. To carry HTTP routes as well as services, add [HardenedWebModule] to the module class and reference Hardened.Web.Runtime and Hardened.Web.SourceGenerator. See Modules.
Versions
Every template writes a Directory.Packages.props with one version for every Hardened package:
<HardenedVersion>0.30.0-rc1000</HardenedVersion>It is the version the template package shipped with, and --hardened-version overrides it. Generated code and the runtime it targets ship together, so the packages move as a set.
Templates do not update themselves. A newer release is a newer template package:
dotnet new install Hardened.Templates # latest
dotnet new install Hardened.Templates@0.30.0-rc1000 # a specific oneExisting projects keep the version in their own Directory.Packages.props until you change it.
The Lambda templates used to float a Hardened.Amz pin, because the AWS packages released from a second repository and for a window an exact pin named a version that did not exist yet. There is one repository and one line now, so every template pins HardenedVersion like everything else.
Each project explains itself
Every generated project carries a README.md on how it runs and how its projects fit together, and an AGENTS.md with the invariants for whoever edits the code. Both are written for the combination you chose.
Next
- Getting started: the same project assembled by hand
- Modules: how
[HardenedModule]composes - Writing a test: what the scaffolded tests do
- AWS: the Lambda runtimes in depth