FTS.Platform.Templates
0.5.0
See the version list below for details.
dotnet new install FTS.Platform.Templates@0.5.0
FTS.Platform.Templates
Two dotnet new templates, so a new service starts already composed against the published
FTS.Platform.* packages instead of being assembled by hand from a reference host.
dotnet new install FTS.Platform.Templates
dotnet new fts-api -n Ordering --db none --auth none
dotnet new fts-module -n Billing -o src/Modules/Billing
They are deliberately thin. Everything they emit is wiring: a composition root, a nuget.config,
one example module to delete, and a test project that points FTS.Platform.ArchitectureRules at the
solution it just created. There are no base classes, no helper types and no conventions folder — if
something here looks reusable, it belongs in a package, and finding one is a finding to report rather
than a file to generate.
The two templates are versioned independently of the platform. What ties them together is
--platform-version, whose default is the platform's current release.
fts-api — the service
A project template. dotnet new fts-api -n Ordering writes:
Ordering.slnx two projects, nothing else
nuget.config <clear/>, the fts feed, nuget.org, no credential
src/Ordering/Ordering.csproj only the packages this composition actually uses
src/Ordering/Program.cs the composition root: one registration per seam
src/Ordering/Example/ the directory to delete - the layered example, six files
src/Ordering/Example/ExampleModule.cs the module's composition root
src/Ordering/Example/Domain/Widget.cs the entity that carries the rule
src/Ordering/Example/Application/ ArchiveWidget (IWriteHandler) and IWidgetStore
src/Ordering/Example/Infrastructure/ the in-memory store you replace
src/Ordering/Example/Api/ two routes, both AllowAnonymous
src/Ordering/appsettings.json the keys this composition needs, and no values it may not choose
src/Ordering/Properties/launchSettings.json the dev-run profile: http://localhost:5080, Development
tests/Ordering.Tests/Ordering.Tests.csproj
tests/Ordering.Tests/BoundaryTests.cs the new solution enforcing its own boundaries
tests/Ordering.Tests/ExampleDomainTests.cs the emitted rule, in memory - deleted with Example/
-n names the solution, the host project, its namespace-free root and the test project; -o decides
where they land. With no flags at all you get the none composition below.
Parameters
The first five are five seams of
ADR 0007,
one flag each. The last two — --platform-version and --port — name no seam and register no
provider. dotnet new fts-api --help prints every default listed here.
| Parameter | Choices | Default | What the emitted Program.cs registers |
|---|---|---|---|
--auth |
none, jwt |
none |
none: AnonymousCurrentActor as a Singleton. jwt: AuthOptions.Resolve as the host's first statement, then AddHttpContextAccessor(), AddTokenAuthentication(auth), UseAuthentication() and UseAuthorization(). |
--db |
none, sqlite |
none |
sqlite: SqliteConnectionFactory behind IDbConnectionFactory, SystemClock, and DatabaseMigrator.Run after Build(). none: no factory and no migrator. It no longer decides the event seam — that is --events — and it still leaves IClock unregistered, which is a known gap rather than a choice. |
--tenancy |
fixed, claims |
fixed |
fixed: SingleTenantContext holding one constant TenantRef — replace both of its literals, see below. claims: nothing extra; AddTokenAuthentication already registered the claims provider. |
--events |
none, inproc |
none |
none: NullEventPublisher as a Singleton — a registered IEventHandler<T> is never called. inproc: InProcessEventPublisher from FTS.Platform.Events as Scoped, since it resolves scoped handlers. This was not a flag before 0.4.0: the publisher lived in FTS.Platform.Sqlite, so --db decided it and no project could take events without a database or a database without events. |
--audit |
none, sqlite |
none |
none: NullAuditLog as a Singleton. sqlite: SqliteAuditLog as Scoped — it takes four dependencies and two of them are Scoped under jwt, so a Singleton would capture them. |
--platform-version |
any version | 0.3.0 |
The version every emitted FTS.Platform.* PackageReference is pinned to. No emitted file carries a literal version number. |
--port |
any port | 5080 |
The loopback port in the emitted Properties/launchSettings.json. Registers nothing: no source file reads it, and a published binary never reads launchSettings.json at all. It is 5080 rather than ASP.NET Core's own 5000 because 5000 is what every other unconfigured host on the machine is already using, and a parameter rather than a constant because a constant just moves the collision to "every project scaffolded from this template". integer, so a non-numeric value is refused at scaffold time. |
--db none --auth none therefore produces the none host. As emitted that is four
FTS.Platform.* packages and five resolved libraries — Abstractions, .Hosting, .Defaults and
.Application, the fourth taken for the layered example in src/<Name>/Example/ and for nothing
else. After the deletion the template documents — the Example/ directory, its test file, that
test project's ProjectReference and the FTS.Platform.Application PackageReference — it is
exactly FTS.Platform.Abstractions, .Hosting and .Defaults, four resolved libraries. Both are
no third-party package at all. That is ADR 0007's "Abstractions + Hosting + Defaults is the whole
cost of wanting none of the seams", scaffolded rather than described.
Authorization is deliberately not a parameter and not registered. RequirePermission without
AddPermissionAuthorization fails at request time, so no emitted endpoint names a permission. A
jwt scaffold still refuses an unauthenticated request, because the fallback policy
AddTokenAuthentication installs needs no authorization provider. Adding the authorization seam is a
deliberate later step, not a flag.
Two combinations are refused, at scaffold time
Both exit 102, name the option, and write no file — rather than generating a project that fails to compile or, worse, one that starts and cannot resolve a dependency.
| Refused | Why |
|---|---|
--tenancy claims without --auth jwt |
Claims tenancy reads the tenant from a validated token claim. Without jwt there are no claims to read. |
--audit sqlite without --db sqlite |
SqliteAuditLog takes an IDbConnectionFactory and an IClock among its four constructor parameters, and both arrive only with the SQLite provider. |
dotnet new fts-api --help prints each refusal as a parameter with an Enabled if: condition, so the
rule is documented by the same lines that enforce it.
What a --auth jwt scaffold needs before it starts
AuthOptions.Resolve runs as the host's first statement and refuses, naming the missing key,
rather than starting a host that would reject every token at request time. The emitted
appsettings.json therefore carries the key NAMES and, for three of them, no value:
| Key | Emitted as | Who supplies it |
|---|---|---|
Auth:Mode |
"jwt" |
Pinned by the template. An absent Mode resolves to header mode in Development, and this host registered only the jwt providers. |
Auth:Audience |
"" |
You. Every token is checked against it. |
Auth:Authority |
"" |
You — the OIDC authority whose JWKS supplies signing keys. |
Auth:TenantKind |
"" |
You. The platform ships no tenant-kind constants: that vocabulary belongs to your project, which is exactly why the template will not choose one. |
For offline use, delete Auth:Authority and supply Auth:Issuer and Auth:SigningKey instead — a
symmetric key of at least 32 UTF-8 bytes. Configure exactly one of Authority and SigningKey;
both is ambiguous and AuthOptions.Resolve says so.
No Auth:SigningKey is emitted, blank or otherwise. This package is public, and a key-shaped
literal in a public template is the one thing here that could become a real credential. Any of these
keys can also arrive from the environment as Auth__Audience, Auth__Issuer, Auth__SigningKey,
Auth__TenantKind, which override the file.
What a --tenancy fixed scaffold must replace
builder.Services.AddSingleton<ITenantContext>(new SingleTenantContext(new TenantRef("tenant", "default")));
Both literals are placeholders. "tenant" is the kind and "default" is the id, and they are
the generic words for the slots rather than words about anything. The fixed provider takes a
constant, so some pair has to be emitted; choosing a meaningful one would be this package deciding
your vocabulary for you, which is the one thing the platform refuses to do anywhere else.
fts-module — a module project
An item template. dotnet new fts-module -n Billing -o src/Modules/Billing writes ten files into
that directory, in the layered shape
ADR 0011
records:
Billing.csproj Abstractions + Application + Hosting, and the Migrations glob
BillingModule.cs the IModule implementation - the composition root
Domain/Widget.cs the entity that carries the rule
Application/ArchiveWidget.cs the use case: ArchiveWidgetCommand + IWriteHandler
Application/IWidgetStore.cs the persistence port
Infrastructure/InMemoryWidgetStore.cs the mechanism, and the one file you replace first
Api/BillingEndpoints.cs two routes under /billing, both AllowAnonymous
Migrations/001_initial.sql one script, so the glob is matching something real
Tests/Billing.Tests.csproj xunit.v3, and a reference to the module beside it
Tests/WidgetTests.cs the emitted rule, in memory: no host and no database
The store is a dictionary and not a database by measurement, not by timidity:
IDbConnectionFactory and IClock are registered only by --db sqlite, so a store injecting either
would throw at builder.Build() in Development — where ValidateOnBuild is on — in exactly the
composition a developer scaffolds first. The same module therefore compiles and runs under every
combination of the four seam flags.
| Parameter | Default | Effect |
|---|---|---|
--module-id |
the lowercased -n name |
The IModule.Id. -n Billing alone yields billing. |
--platform-version |
0.3.0 |
As above. |
IModule.Id is contracted stable and lowercase: it is the key AddModules registers the module
under, and it lands in the module_id column of every audit row the module writes. The default is
generated from the name rather than typed, so it is never the template's own word — but it is
lowercased without hyphenating, so -n InventoryTracking yields inventorytracking. A
multi-word id is what --module-id is for:
dotnet new fts-module -n InventoryTracking -o src/Modules/InventoryTracking --module-id inventory-tracking
Because that default is generated rather than literal, dotnet new fts-module --help prints
Type: string for --module-id with no Default: line. The emitted Id is where you can see it.
It modifies no existing file
The template writes into its output directory and nowhere else. Registering the module in a host is
four edits, and they are emitted as the header comment of the generated *Module.cs with your own
names already substituted:
1. dotnet sln add src/Modules/Billing/Billing.csproj
2. dotnet sln add src/Modules/Billing/Tests/Billing.Tests.csproj
3. dotnet add src/<host>/<host>.csproj reference src/Modules/Billing/Billing.csproj
4. in the host's Program.cs, with no `using` at all:
builder.Services.AddModules(builder.Configuration, new Billing.BillingModule());
The argument is namespace-qualified and there is no using, so the documented manual edit and the one
fts add module makes are the same edit.
fts add module does that for you. It is still not this package's job: an item template that
reached out of its output directory to edit files it did not create is a template you could not run
twice, and could not read the diff of. The split is the reason the tool exists at all.
The example module fts-api emits is a directory of files in the host project, not a project —
which is what keeps the none scaffold at five resolved libraries as emitted and four after the
deletion. fts-module emits a project because that is the shape a module that is not an example has.
Restoring what a scaffold references
The emitted nuget.config declares two sources and no credential: the FTS feed, and nuget.org —
which is required rather than optional, because FTS.Platform.Sqlite carries Microsoft.Data.Sqlite,
dbup-sqlite and Dapper, FTS.Platform.Auth carries
Microsoft.AspNetCore.Authentication.JwtBearer, and the test project takes xunit.v3,
xunit.runner.visualstudio and Microsoft.NET.Test.Sdk. None of those is on the FTS feed. The
none composition takes nothing from nuget.org at all.
GitHub Packages has no anonymous read for NuGet: even a public package needs a token. A 401 on
restore means no token reached NuGet; a 403 on the download after a 200 on the index means the token
lacks the read:packages scope. Keep the credential out of source control — supply it from the
environment, or from a nuget.config that is not committed.
Uninstalling
dotnet new uninstall FTS.Platform.Templates
-
.NETStandard 2.0
- No dependencies.
GitHub repositories
This package is not used by any popular GitHub repositories.