Skip to content
purview-devPublic

About

Aspire extension that generates live LikeC4 C4 architecture diagrams from the Aspire resource graph, with a Roslyn source generator for compile-time validation of tags, kinds, groups, and metadata.

Topics

Resources

Contributing

Stars

9 stars

Watchers

0 watching

Forks

Latest commit

 

History

159 Commits

Folders and files

Repository files navigation

AspireC4.Hosting

NuGet version Release

AspireC4.Hosting is an Aspire extension library that generates live LikeC4 diagrams from the Aspire resource graph.

Prerequisites

  • .NET 8 or later.
  • Aspire 13.5.3 or later. Aspire AppHost projects must reference both the AppHost SDK and Aspire.Hosting.AppHost.
  • Docker is used by default to run the LikeC4 sidecar container.
  • Optional: a local Node.js CLI runtime (npx, pnpm, yarn, bun, or deno) if you call .WithLocalCLI().

Installation

Add AspireC4 to the AppHost project:

dotnet add package AspireC4.Hosting
dotnet add package Aspire.Hosting.AppHost

An Aspire 13.5 AppHost project should contain the equivalent of:

<Project Sdk="Aspire.AppHost.Sdk/13.5.3">
  <ItemGroup>
    <PackageReference Include="Aspire.Hosting.AppHost" />
    <PackageReference Include="AspireC4.Hosting" />
  </ItemGroup>
</Project>

Quick start

var builder = DistributedApplication.CreateBuilder(args);

builder.AddAspireC4();

builder.Build().Run();

This writes ./.aspirec4/likec4/model.gen.c4, starts the LikeC4 server, and refreshes the diagram as the Aspire app changes.

Note

Upgrading from 13.5 or earlier? Generated output moved to ./.aspirec4/ and, once a second diagram is registered, the architecture diagram's dashboard resource is renamed. For an upgrade that changes nothing else, call options.WithLegacyLayout() — it restores both the old paths (./likec4/gen/, ./erd/gen/) and the flat aspirec4 / aspirec4-er names. See Upgrading from before 13.6.

Configuration

Configure the diagram through AspireC4DiagramOptions:

Property Default Description
Title null Title shown in the LikeC4 app
ViewTitle "Architecture" Title shown in the generated view
ViewDescription null Optional view description
OutputDirectory "./.aspirec4/likec4/" Directory where the generated .c4 file is written
ProjectDirectory null An existing LikeC4 project to generate into and serve — see Using your own LikeC4 project
UseLegacyLayout false Restores the pre-13.6 paths and dashboard names — see Upgrading from before 13.6
FileName "model.gen" Generated file name without extension
DisableHMR false Disable Hot Module Replacement
HMRPort null (dynamically allocated) HMR port used by the LikeC4 server and browser when it supports configurable HMR ports; set a fixed port when needed
ContainerImageTag null (latest) Pin the ghcr.io/likec4/likec4 image tag
AutoIconsEnabled true Infer LikeC4 icons from resource type and name
HideFromDashboard false Hide the LikeC4 sidecar from the Aspire dashboard
DashboardLinkDisplayName "Architecture Diagram" Name used for the diagram link when hidden from the dashboard
IncludeAspireDashboardLinks true Add Aspire dashboard links to diagram elements

Common options

Local CLI

builder.AddAspireC4().WithLocalCLI();

Hide from the dashboard

builder.AddAspireC4(options => options.WithHideFromDashboard());

Disable HMR

builder.AddAspireC4(options => options.WithHMRDisabled());

Exclude the sidecar from the diagram

The LikeC4 sidecar is excluded automatically. Set WithIncludeAspireC4InternalResource(true) in the options callback if you want to inspect it.

Upgrading from before 13.6

13.6 changed two things, both breaking:

  • Generated output moved under one root: ./.aspirec4/likec4/ (was ./likec4/gen/) and ./.aspirec4/erd/ (was ./erd/gen/). Replace your .gitignore entries with **/.aspirec4/.
  • When more than one diagram is registered, they nest under a shared dashboard parent and the architecture diagram's resource is renamed from aspirec4 to aspirec4-likec4. With only AddAspireC4(), nothing changes.

Legacy mode (keeping the pre-13.6 layout)

Both changes revert together with one switch, for a zero-change upgrade:

builder.AddAspireC4(configure: options => options.WithLegacyLayout());

or, without touching code:

{
  "AspireC4": {
    "UseLegacyLayout": true
  }
}

With legacy mode on, the architecture diagram writes to ./likec4/gen/, the ER diagram to ./erd/gen/, and the diagrams stay flat in the dashboard as aspirec4 and aspirec4-er with no parent row. An OutputDirectory you set explicitly still wins over the legacy default, and the switch does not affect WithLikeC4ProjectFolder(...) — generated output always stays beneath a hand-authored project.

It is a compatibility shim that will be removed in a future major version — prefer adopting the new layout. The full details, including what to update when you drop it, are in the migration guide.

Using your own LikeC4 project

Point AspireC4 at a LikeC4 project you already maintain and it serves that directory instead of its own: your likec4.config.json, .c4 sources and views are all in play, and the generated model joins them as one more file. AspireC4 writes to .aspirec4/ beneath the project and never generates a config over yours.

builder.AddAspireC4(configure: options => options
    .WithLikeC4ProjectFolder("../../architecture"));

builder.AddProject<Projects.Api>("api")
       // Attach Aspire's live state to the element the project already declares,
       // rather than adding a duplicate beside it.
       .WithLikeC4Element("cloud.backend.api");

The generated model emits an extend block for a mapped resource:

model {
  extend cloud.backend.api {
    #aspire-run-state-running
    link http://localhost:5123 'Endpoint: http'
    metadata { aspire-name 'api' }
  }
}

Only tags, links, metadata, relationships and nested elements are applied — LikeC4's extend body accepts nothing else, so your project stays the single source of each element's title, technology, description and icon. See Generated Output for the details.

Entity Framework Core ER diagrams (d2)

AspireC4.Hosting.EntityFrameworkCore adds a second, complementary diagram: a live d2 sql_table ER diagram generated from your Entity Framework Core DbContext types, hosted alongside the architecture diagram and linked from it.

The AppHost package is all that is required. WithEntityFrameworkDiagram() finds every associated DbContext automatically — reading a build-time dump or a source-generated descriptor where a project carries one, and falling back to the assembly's metadata otherwise:

# AppHost project
dotnet add package AspireC4.Hosting.EntityFrameworkCore
var builder = DistributedApplication.CreateBuilder(args);

builder.AddAspireC4()
       .WithEntityFrameworkDiagram(configure: options => options
           .LayoutEngine = D2LayoutEngine.Elk);

builder.AddProject<Projects.Api>("api");

builder.Build().Run();

Two packages upgrade a project's fidelity, and both are opt-in by reference alone — see How the model is read:

# The project that owns the DbContext and that you control: captures fluent configuration
dotnet add package AspireC4.EntityFrameworkCore

# The project that owns the EF Core provider and a design-time DbContext factory:
# writes the real model, composite keys included
dotnet add package AspireC4.EntityFrameworkCore.ModelDump

This writes ./.aspirec4/erd/schema.gen.d2, starts a d2lang/d2 sidecar running d2 --watch, and adds an ER Diagram link to each contributing project in the dashboard and on its element in the architecture diagram.

Dashboard layout

Both diagrams write beneath a single ./.aspirec4/ root in the AppHost directory — one directory to ignore in source control regardless of how many are enabled.

Note

The ER diagram's ./.aspirec4/erd/ default follows the architecture diagram's legacy switch: WithLegacyLayout() restores ./erd/gen/ too, because the two diagrams must agree. An OutputDirectory set on WithEntityFrameworkDiagram still wins. See Legacy mode.

With only the architecture diagram registered, the dashboard shows the one aspirec4 row it always has. Registering a second diagram nests them under a shared parent instead:

aspirec4                 # parent row, added only when there is more than one diagram
├── aspirec4-likec4      # the architecture diagram
└── aspirec4-er          # the ER diagram

Configuration

Property Default Description
Renderer Interactive Interactive, Mermaid, Dbml or D2 — see Renderers
Title null Title shown in the generated diagram
DiscoverAllProjects true Scan every project resource for DbContext types. WithProjectOptInOnly() restricts it to projects marked with WithEntityFrameworkModel()
IncludedContexts empty When non-empty, only these contexts appear. WithIncludedContexts(...)
ExcludedContexts empty Contexts to omit, matched by short or fully-qualified name. WithExcludedContexts(...)
OutputDirectory "./.aspirec4/erd/" Directory the generated files are written to. Follows the architecture diagram's legacy switch when left at its default
FileName "schema.gen" Generated file name without extension
UseSubgraphs true Mermaid only — group entities into one subgraph per DbContext (needs Mermaid 11.17+)
LayoutEngine Elk D2 only. Dagre, Elk or Tala. dagre cannot target sql_table rows
ShowNullability true Render nullable columns with a trailing ?
GroupBySchema true Group tables into d2 containers by database schema
GroupByContext true Group by DbContext when more than one is discovered
DomainTypeDisplay DomainTypeFirst How a column shows its domain type — a value object or an enum — beside the database type: DomainTypeFirst (AssetId (guid), ActorType (int, enum)), DatabaseFirst, DomainTypeOnly, DatabaseOnly
ContainerImageTag null (v0.9.0) Pin the d2lang/d2 image tag
UseViewerServer true Serve the viewer so the dashboard can link to it; set false where there is no container runtime
ViewerServerImageTag null (1.29-alpine) Pin the nginx image tag used to serve the viewer
HideFromDashboard false Hide the d2 sidecar from the Aspire dashboard
DashboardLinkDisplayName "ER Diagram" Name used for the diagram link

Renderers

Every text format is a lossy projection of the model. Only the interactive renderer consumes the descriptor directly, which is why it is the default.

Renderer Output Fidelity Use it for
Interactive (default) .json + viewer Full — column-level edges, grouping, key badges, crow's-foot cardinality The primary experience
Dbml .dbml High — column-level refs and precision types survive Interchange: dbdiagram.io, SQL generation
Mermaid .mmd + preview Low — edges join entities, not columns; one flat grouping level Publishing: GitHub, Azure DevOps, wikis
D2 .d2 + live sidecar High — column-level edges, nested containers Live local viewing

Known limitations, by renderer:

  • Mermaid emits a local preview alongside the .mmd for eyeballing the output before pasting it somewhere that renders Mermaid natively. The preview is deliberately minimal — render, pan, zoom, theme and export — because Mermaid's job here is publishing, not being the primary experience. It cannot attach an edge to a column, so the participating columns are carried in the edge label instead. Its attribute-type grammar permits parentheses but not commas, so decimal(18,2) is emitted as decimal(18_2) — the precision is kept rather than dropped. Grouping is one flat subgraph level, so the schema tier is lost. GitHub was confirmed to render the subgraph output in October 2026; set UseSubgraphs = false for a renderer pinned to a Mermaid older than 11.17.
  • DBML renders nowhere on its own — it is an interchange format for tools such as dbdiagram.io.
  • D2 needs the container (or a local d2 binary) and renders nowhere else.
  • Interactive needs its JavaScript dependencies vendored once per output directory — see below.

Cardinality mapping

EF's own IsUnique/IsRequired are runtime concepts, so both are derived statically: a foreign key is unique when its columns are the primary key or carry a unique index, and required when they are non-nullable. The principal side is always ||, because a foreign key references exactly one row.

The interactive renderer draws the same mapping as crow's-foot notation, so both renderers say the same thing about a relationship.

Unique Required Mermaid Meaning
yes yes ||--|| one-to-one, mandatory
yes no ||--o| one-to-zero-or-one
no yes ||--|{ one-to-one-or-many
no no ||--o{ one-to-zero-or-many

Lines are always identifying (--). Delete behaviour and ownership are runtime EF concepts that static analysis cannot recover, and guessing non-identifying would be misleading.

Using the viewer

The interactive viewer is a tool for reading a schema, not just a picture of one.

Search (/ or Ctrl/Cmd+K to focus) matches table and column names. Matching tables are highlighted, everything else — including the relationships into it — recedes, and the row that caused a match is marked on the card, so a hit on a column is visible without hunting for it. The counter shows your position in the results; Enter travels to the next match and Shift+Enter to the previous, bringing each into view. Esc clears.

Relationships are labelled with the relationship's name in both directions — Blog ⇄ Posts — so a direction the model does not declare shows as ? rather than being hidden. Where neither navigation exists it falls back to the foreign key's constraint name, then its columns. Edges are drawn from the model, so nothing can be connected by hand: the handles are anchors, not a drawing surface.

Layout. Drag a table and the box around its DbContext follows, growing or shrinking to wrap its tables; the arrangement is remembered per model, so a reload keeps it and a regenerated diagram (whose model has changed) falls back to a fresh layout.

View (the toolbar's View control) chooses how much of each table is drawn: Columns (the default — every column with its type and key badges) or Names (the table name alone, for reading a large schema at a glance).

Settings (the toolbar's Settings button) holds the theme (Auto, the default, following the operating system; Light; Dark), the edge animation speed (or off), the background pattern, and a motion override. A pinned theme is applied before the page paints, so there is no flash of the wrong theme. The Mermaid preview shares the theme — pinning it in one applies it to both.

Export writes PNG or SVG of the whole diagram at its natural size, not a capture of the window, so nothing scrolled out of view is lost. PNG is rendered at 2× for printing and zooming. Both honour the current theme, so a diagram exported in dark mode is dark.

The Mermaid preview carries the same theme control and the same PNG/SVG export; beyond that it stays deliberately minimal — render, pan, zoom — because Mermaid's job here is publishing. Note that animated edges are browser-only and do not appear in any exported image.

Recipes

just erd-vendor --embed               # rebuild the viewer's bundled JS after a dependency bump
just erd-check                        # drive the interactive viewer in a real browser
just erd-mermaid-check                # drive the Mermaid preview in a real browser
just erd-svg .aspirec4/erd/schema.gen.mmd   # render Mermaid to SVG via mermaid-cli 12 in a container

How the viewer is served

The interactive and Mermaid renderers write static files rather than running a server, so a small nginx sidecar serves the output directory. That is what gives the resource a real URL in the dashboard — a file:// link cannot be navigated to from the dashboard's own origin — and it makes every generated export downloadable from the same place.

Set UseViewerServer = false where no container runtime is available. The diagram is still generated, and the dashboard offers a command that opens it locally instead.

The viewer's JavaScript ships with the package, gzipped, and is written out beside the diagram on first generation — there is no setup step. Only what the chosen renderer needs is written.

The viewer loads its dependencies from ./vendor/ rather than a CDN, so a generated diagram keeps working offline. They are genuinely bundled with bun build; fetching ESM modules from a CDN at vendoring time does not work, because the responses are re-export stubs pointing at absolute CDN paths.

Existing files are never overwritten, so a locally rebuilt bundle survives regeneration. Run bunx playwright install chromium once before just erd-check.

Animated edges are browser-only and will not appear in the SVG output.

How the model is read

Every project resource is scanned for DbContext types. Each context is recovered from the best source available, and the diagram records which one was used:

Source When Fidelity
Dump The project — or one it references — references AspireC4.EntityFrameworkCore.ModelDump Full — the real EF model, composite foreign keys included
Descriptor The project references the analyzer-only AspireC4.EntityFrameworkCore package Full — includes fluent OnModelCreating configuration
Metadata Otherwise Approximate — conventions and data annotations only

The metadata path opens the built assembly with a reflection-only load context, so no code in it runs — the same guarantee as the descriptor path, which reads the attribute straight out of assembly metadata. This is what makes WithEntityFrameworkDiagram() work with no per-project configuration, and it is the only way a shared or third-party assembly — one you cannot add a package to — contributes at all.

Because OnModelCreating is a method body, it is absent from metadata: a project without the analyzer still appears, but fluent renames, default schemas and Ignore calls do not. MetadataContextsFound in the loader diagnostics, and the Approximate fidelity on each context, record which happened, so a partial model is never mistaken for a complete one.

Filtering

WithExcludedContexts(...) and WithIncludedContexts(...) accept short names (IdentityDbContext) or fully-qualified type names (Shared.Data.IdentityDbContext), case-insensitively; an exclusion always wins. This is how a context shared by several projects — an IdentityDbContext in a common library nobody owns — is kept out without editing that library. The same filters are available per project through the WithEntityFrameworkModel(options => ...) overload. Because the same context can reach the diagram from more than one project, contexts are de-duplicated by fully-qualified type name; when both an approximate and a full description exist, the full one wins.

Authoritative dump (opt-in)

The two recovered sources above never touch the service. Where you own the project and want the exact model — composite foreign keys included — add the dump package to the project that owns the EF Core provider and a design-time DbContext factory:

dotnet add package AspireC4.EntityFrameworkCore.ModelDump

That reference is the opt-in. The build then runs the dump tool, which builds each context's real IModel and writes the diagram contract to obj/aspirec4/ef/. That directory is never copied to bin/ or publish/ and is gitignored, so the dump has no deployment footprint. The AppHost reads it — from that project and from every project it references — in preference to the descriptor and the metadata reader, and falls back when it is missing or older than the built assembly.

Set <AspireC4EfModelDumpConnectionString> when the design-time factory needs one to build its options — the model is never opened, so any syntactically valid value works.

Because the model is recovered statically, it is a close approximation rather than a guaranteed match for the migrated schema.

Recovered from metadata (every project):

  • DbSet<T> properties, and entity types reachable through navigation properties.
  • [Key], [Required], [MaxLength], [StringLength], [Table], [Column], [ForeignKey], [NotMapped], [Index(IsUnique = true)].
  • EF naming conventions for primary keys (Id / <Type>Id) and foreign keys (<Nav>Id / <PrincipalType>Id).
  • A column's domain type — a value object or an enum — is shown beside its real database type, controlled by DomainTypeDisplay: AssetId (guid) for a value object, ActorType (int, enum) for an enum. A scalar value object (a struct wrapping a single Value, such as a strongly-typed identifier) becomes a column, and its foreign keys carry the relationships between entities. A complex value object — one marked [ValueObject(EFMapping = ComplexType)] — is flattened into its owner's table. An enum is shown as itself and marked enum. The primitive a scalar value object converts to is known only to its value converter, so the model keeps both.

Also recovered from a descriptor (analyzer-referenced projects):

  • Fluent configuration in OnModelCreating: HasDefaultSchema, ToTable, HasKey, Ignore, HasIndex(...).IsUnique(), and Property(...) with HasColumnName, HasColumnType, HasMaxLength, IsRequired, HasComment.
  • Column comments, from [Comment] or HasComment.

Derived in both cases:

  • Relationship cardinality and join tables, both derived rather than read from EF: a foreign key is unique when its columns are the key or carry a unique index, required when they are non-nullable, and a table is a join table when every key column is also a foreign key and there is more than one.
  • Relationships themselves are derived from navigation properties and the <Nav>Id convention. A relationship configured only fluently — with no navigation, or a composite foreign key whose columns do not match that convention — is not drawn.

Not captured. These need either runtime model-building or analysis beyond what is sensible statically, and are silently absent rather than guessed at: owned types, TPH/TPT/TPC inheritance mapping, delete behaviour, sequences, check constraints, and value conversions. Relationships that cross a DbContext boundary are deliberately not drawn — EF models a relationship inside one context, so an edge across that boundary would be invented rather than observed.

Configuration applied indirectly — most notably ApplyConfigurationsFromAssembly — cannot be seen by static analysis and is reported as ASPIREC4010. Store types are generic (int, varchar, guid, …) rather than provider-specific, since the provider is not knowable at compile time; an explicit [Column(TypeName = "...")] or HasColumnType("...") always wins.

Local CLI instead of a container

builder.AddAspireC4()
       .WithEntityFrameworkDiagram()
       .WithLocalD2CLI();          // after WithEntityFrameworkDiagram

d2 ships as a Go binary. Unlike the LikeC4 sidecar, there is no npm CLI fallback — the @d2lang/d2 npm package is a WASM library with no executable. Install d2 (scoop, Homebrew, install.sh, or go install) so it is on PATH, or pass an explicit path: WithLocalD2CLI("/opt/d2/bin/d2").

Overriding the output directory

Prefer the configure callback over configuration for OutputDirectory. The sidecar's bind mount is fixed when the AppHost builder runs, so a value supplied through configuration changes where the file is written but not what the container serves. The callback is applied in time for both.

Aspire TypeScript AppHost support

AspireC4 supports both C# and TypeScript Aspire AppHosts. In a TypeScript AppHost, the Aspire integration exports the same diagram configuration, resource metadata, grouping, and relationship features through camel-cased asynchronous APIs. The C# registry source generator described later is not applied to TypeScript; TypeScript applications configure tags, kinds, groups, and metadata through the generated fluent API.

The Aspire CLI generates the TypeScript API surface under .aspire/modules/. Import createBuilder from the generated Aspire module, then add AspireC4 to the builder:

import { createBuilder } from "./.aspire/modules/aspire.mjs";

const builder = await createBuilder();

await builder
    .addAspireC4({
        configure: async (options) => {
            options
                .withTitle("My distributed application")
                .withViewTitle("Architecture")
                .withViewDescription("Generated from the Aspire resource graph");
        },
    })
    .configureServer(async (resource) => {
        resource.withLikeC4Details({
            configure: async (options) => {
                options.withLabel("Architecture diagram");
            },
        });
    });

const api = await builder.addNodeApp("api", "../api", "index.ts");

await api.withLikeC4Details({
    configure: async (options) => {
        options
            .withLabel("API")
            .withTechnology("Node.js")
            .withTag("backend");
    },
});

const app = await builder.build();
await app.run();

Declare the integration and its Aspire dependencies in aspire.config.json:

{
  "appHost": {
    "path": "apphost.mts",
    "language": "typescript/nodejs"
  },
  "sdk": {
    "version": "13.5.3"
  },
  "packages": {
    "Aspire.Hosting.JavaScript": "13.5.3",
    "AspireC4.Hosting": "13.5.3"
  }
}

Use the AspireC4 package version appropriate for the application. The repository sample points AspireC4.Hosting at ../../src/src/AspireC4/AspireC4.csproj so it exercises the local source instead of a published package.

Run the TypeScript sample

The sample at samples/typescript-app-host demonstrates:

  • AspireC4 configuration from apphost.mts.
  • Azure Redis and PostgreSQL resources running as local containers.
  • Redis Commander and PgWeb dashboard resources.
  • A TypeScript Node.js service with Redis and PostgreSQL references.
  • LikeC4 labels, descriptions, links, icons, metadata, tags, groups, and relationships.
  • Additional LikeC4 DSL and image folders from this repository's assets directory.

Prerequisites are Docker, the Aspire CLI, and Bun. From the repository root:

cd samples/typescript-app-host
bun install
aspire restore
aspire start

aspire restore restores the integrations declared in aspire.config.json and regenerates .aspire/modules/. Once aspire start completes, open the Aspire dashboard URL printed by the CLI and select the LikeC4 resource or its architecture-diagram link. The sample also exposes the node-app /health, /ping/redis, and /ping/postgres endpoints through Aspire-assigned URLs.

For an interactive foreground session, the sample's Bun script is equivalent to aspire run:

bun run dev

Stop a background session with:

aspire stop

Generated TypeScript modules

Do not edit files under .aspire/modules/; Aspire owns and regenerates them. If the folder is missing or stale after a pull, clean, or branch switch, run:

aspire restore

When adding another Aspire integration, use aspire add <package> so Aspire updates aspire.config.json and regenerates the TypeScript API. Inspect .aspire/modules/aspire.mts to see the APIs currently available to apphost.mts.

Compile-time registry validation

AspireC4 includes an incremental source generator built on Purview.SourceGeneratorFramework. It can validate constant values passed to:

  • .WithTag()
  • .WithKind()
  • .WithLikeC4Group()
  • .WithMetadata()

The generator injects the registry attributes and enums automatically. Do not declare or reference a separate source generator package.

Registry class

Add one [LikeC4Registry] class to the AppHost assembly. Its accessibility and nesting do not matter. Values are declared as const string fields in conventionally named nested classes:

using Aspire.Hosting.AspireC4;

[LikeC4Registry]
internal static class ArchitectureRegistry
{
    public static class Tags
    {
        public const string External = "external";
        public const string LocalDevelopment = "local-dev";
    }

    public static class ElementKinds
    {
        public const string Service = "service";
    }

    public static class RelationshipKinds
    {
        public const string Async = "async";
    }

    public static class Groups
    {
        public const string Platform = "Platform";
    }

    public static class MetadataKeys
    {
        public const string AzureSku = "Azure_SKU";
    }
}

Supported nested-class names are:

Registry type Accepted class names
Tag Tag, Tags
Element kind ElementKind, ElementKinds, Element, Elements
Relationship kind RelationshipKind, RelationshipKinds, Relationship, Relationships
Group Group, Groups
Metadata key MetadataKey, MetadataKeys

Use the constants at call sites to make refactoring safe:

builder.AddProject<Projects.Api>("api")
    .WithLikeC4Details(details =>
        details
            .WithTag(ArchitectureRegistry.Tags.External)
            .WithKind(ArchitectureRegistry.ElementKinds.Service)
            .WithMetadata(ArchitectureRegistry.MetadataKeys.AzureSku, "Standard_LRS")
    )
    .WithLikeC4Group(ArchitectureRegistry.Groups.Platform);

Individual registry fields

For a flat registry, annotate each constant with [KnownType]:

[LikeC4Registry]
internal static class ArchitectureRegistry
{
    [KnownType(LikeC4RegistryType.Tag)]
    public const string External = "external";

    [KnownType(LikeC4RegistryType.Group, Strict = LikeC4Severity.Warning)]
    public const string Platform = "Platform";
}

Do not declare the same registry type using both a named nested class and [KnownType] fields. Doing so produces ASPIREC4005.

Validation severity

Without an explicit strict setting, a registry class enables suggestion-level validation. Severity can be configured at three levels, from broadest to most specific:

  1. The AspireC4Strict MSBuild property.
  2. [LikeC4Registry(Strict = ...)] for the registry.
  3. [Severity(...)] on a named nested class, or KnownType.Strict on an individual field.
[LikeC4Registry(Strict = LikeC4Severity.Warning)]
internal static class ArchitectureRegistry
{
    [Severity(LikeC4Severity.Error)]
    public static class Tags
    {
        public const string External = "external";
    }

    [KnownType(LikeC4RegistryType.Group, Strict = LikeC4Severity.Off)]
    public const string UnvalidatedGroup = "Temporary";
}

LikeC4Severity supports Inherit, Off, Suggestion, Warning, and Error.

The project-wide setting can be placed in the AppHost project or Directory.Build.props:

<PropertyGroup>
  <AspireC4Strict>warning</AspireC4Strict>
</PropertyGroup>

Accepted AspireC4Strict values are:

Value Behavior
off or an unset/unknown value Disables DSL-file strict validation
suggestion Reports undeclared DSL values as suggestions
warning Reports undeclared DSL values as warnings
error, true, yes, or all Reports undeclared DSL values as errors
allincludingmetadata Error-level validation including metadata keys

Metadata-key comparison is case-insensitive and normalizes punctuation and whitespace to underscores. For example, Azure SKU, azure sku, and Azure_SKU identify the same key.

To disable all AspireC4 source-generator diagnostics while retaining the injected registry types:

<PropertyGroup>
  <DisableAspireC4SourceGenerator>true</DisableAspireC4SourceGenerator>
</PropertyGroup>

Validate LikeC4 files

Add .c4 or .likec4 specification files as compiler additional files, then set AspireC4Strict:

<ItemGroup>
  <AdditionalFiles Include="likec4/**/*.c4" />
  <AdditionalFiles Include="likec4/**/*.likec4" />
</ItemGroup>

<PropertyGroup>
  <AspireC4Strict>warning</AspireC4Strict>
</PropertyGroup>

Tags, element kinds, and relationship kinds in specification blocks are merged with registry-class definitions.

Diagnostics

ID Meaning
ASPIREC4001 A tag passed to .WithTag() is undeclared
ASPIREC4002 An element or relationship kind passed to .WithKind() is undeclared
ASPIREC4003 More than one class in the assembly has [LikeC4Registry]
ASPIREC4004 A group passed to .WithLikeC4Group() is undeclared
ASPIREC4005 A registry type uses both a nested class and [KnownType] fields
ASPIREC4006 A metadata key passed to .WithMetadata() is undeclared
ASPIREC4007 A nested class in the registry uses a name that is not a recognized registry type

Breaking changes in the Source Generator Framework migration

The source generator now uses the current Purview.SourceGeneratorFramework incremental APIs. Existing applications should review the following changes when upgrading:

  • Aspire AppHost dependency is explicit. Aspire 13.5 AppHosts must reference Aspire.Hosting.AppHost; relying on the AppHost SDK alone produces ASPIRE002.
  • Only one registry class is supported per assembly. Merge multiple [LikeC4Registry] classes into one class.
  • Registry declaration styles cannot be mixed per type. For example, choose either a Tags nested class or [KnownType(LikeC4RegistryType.Tag)] fields. Mixing both now produces ASPIREC4005.
  • Strict settings are severity-based. Replace older boolean-only assumptions with suggestion, warning, error, all, or allincludingmetadata. true remains accepted as an alias for error-level validation.
  • Metadata validation is opt-in at the global level. Use allincludingmetadata, or apply an explicit metadata severity through [Severity]/[KnownType]. Plain all does not validate metadata keys.
  • Generated source files are split by type. The generator now emits LikeC4RegistryAttribute.g.cs, KnownTypeAttribute.g.cs, SeverityAttribute.g.cs, LikeC4RegistryType.g.cs, and LikeC4Severity.g.cs instead of a combined LikeC4RegistryAttributes.g.cs. This affects generator snapshot tests and tooling that inspected hint names; normal application source code is unaffected.
  • Do not define generated registry types manually. Remove compatibility copies of LikeC4RegistryAttribute, KnownTypeAttribute, SeverityAttribute, LikeC4RegistryType, or LikeC4Severity to avoid duplicate-type errors.
  • Generator packaging is automatic. Consumers should reference only AspireC4.Hosting; remove direct references to AspireC4.SourceGenerators or Purview.SourceGeneratorFramework that were added solely to make the AspireC4 generator run.
  • TypeScript APIs are generated by Aspire. TypeScript AppHosts import from .aspire/modules/aspire.mjs; generated files must not be copied between projects or edited manually. Run aspire restore after upgrading AspireC4 so the exported API matches the installed integration version.

Documentation

About

Aspire extension that generates live LikeC4 C4 architecture diagrams from the Aspire resource graph, with a Roslyn source generator for compile-time validation of tags, kinds, groups, and metadata.

Topics

Resources

Contributing

Stars

9 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages