Skip to content

Carrot.Data.Scaffold.Compilers.Actions.EnsureProjects

net

Pipeline action that validates and manages the .NET project structure generated from scaffold data. Creates missing projects from VS templates, sets root namespaces, adds projects to the solution, and wires up project references.

Installation

xml
<ProjectReference Include="..\Carrot.Data.Scaffold.Compilers.Actions.EnsureProjects\Carrot.Data.Scaffold.Compilers.Actions.EnsureProjects.csproj" />

Architecture

Pipeline Integration

This action registers at ScaffoldCompilerPipelineEvent.AfterModules - it runs after all modules have rendered their outputs but before sinks write files. At this point the full bundle is available, so the action knows exactly which projects need to exist.

Project Planning

ProjectPlanner reads the scaffold's API and database graphs to produce a flat list of ProjectNode objects across seven axes:

Project/assembly names and namespaces are rooted at Carrot.Platforms.{PlatformName}.*{PlatformName} is sourced from the .scaffold profile (platformName). Folder paths drop the Platform segment and do not carry the qualifier.

  • Domain - Carrot.Platforms.{PlatformName}.Domain[.Collection[.Context]] — Architect-owned, hand-written semantic code
  • Abstractions - Carrot.Platforms.{PlatformName}.Abstractions[.Collection[.Context]] — Generated, shared interface shapes consumed by Api / Database / Domain (controller capability interfaces today; the slot for any future shared contract types)
  • Database - Carrot.Platforms.{PlatformName}.Database[.DatabaseName[.Context]]
  • Api - Carrot.Platforms.{PlatformName}.Api[.Collection[.ApiName.Models/.Client/controllers]]
  • Contracts - Carrot.Platforms.{PlatformName}.Contracts[.Collection[.Domain]]
  • Application - orchestration projects: Application.Api, Application.Database, Application.Operations, then the top-level Application itself
  • Hosting - Carrot.Platforms.{PlatformName}.Application.Hosting carrying the platform-aware hosting modules (closed-context module subclasses, open-generic service forwarders). Implementation-specific modules — config sources, host-flavoured seeds, public-vs-admin pipeline tweaks — live in Hosting/ folders inside the actual entry-point host projects, not here. CoreHosting's assembly-scoped module discovery means those modules only load for the host that owns them.

Each planner partial class (ProjectPlanning~Domain.cs, ~Abstractions.cs, ~Api.cs, etc.) generates nodes at three tiers: Base (one per axis — the dependency anchor every Collection/Context inherits refs from), Collection (one per collection) and Context (one per domain/API/database context). Whether a node is hand-written or scaffold-generated is a separate dimension — see Ownership below.

Dependency direction: Domain sits at the top with no upstream deps; Abstractions depends on Domain; Api and Database depend on Abstractions (and Database additionally on Domain for entity types); Contracts depends on Api and Database; Application sits at the bottom and consumes Contracts plus the Api/Database axes.

Ownership & run modes

Every ProjectNode carries a ProjectOwnershipArchitect, Generated or Implementer — recording who owns the file contents. Ownership does NOT affect csproj creation — EnsureProjects creates the templated class-lib shell for every missing project regardless of ownership. Ownership only decides whether code gets emitted INTO the shell afterwards:

  • Architect — hand-written, committed: the entire Domain axis ({Platform}.Domain[.Collection[.Context]]) plus the {Platform} root assembly. The scaffold creates the csproj shell, the architect adds .cs files by hand. (Readmes still emit at the project root — those are auto-doc and consume one file.)
  • Generated — scaffold-owned: Abstractions, Database, Api, Contracts axes. Shell + all .cs files come from the scaffold.
  • Implementer — the churny implementation layer (Application + Hosting). Lives in a separate repo; only stamped under implement / full.

ScaffoldRun.Mode (Build | Implement | Full) selects which ownerships a run stamps:

ModeProjects stampedRootCross-set edges
build (default)Architect + Generatedsrc/base/, src/generated/n/a — closed set
implementImplementer only, into an external target solutionsrc/net/Platforms/floating <PackageReference>
fulleverything (monorepo escape hatch)all threestay <ProjectReference>

ProjectBuilder applies the filter immediately after ProjectPlanner.Build() — the planner always produces the full node set; the mode decides what survives into the dependency graph.

Dependency Graph

ProjectBuilder takes the (mode-filtered) planner output and builds a DependencyGraph<ProjectNode> using Carrot.Collections.DependencyGraphs. This ensures projects are created and referenced in topological order. Path resolution (BakeProjectPaths) roots each node by ownership — Generated under the scaffold output root (src/generated/, co-located with its rendered code), Architect under src/base/, Implementer under src/net/Platforms/ — and resolves its relative and absolute paths from there.

Execution Steps

  1. Install VS template - ensures Carrot.ClassLib template is available
  2. Create missing projects - dotnet new from template for any project whose .csproj doesn't exist. Runs across all ownerships — Architect and Generated csprojs are both templated class libs; the difference is whether code gets generated INTO them downstream, not whether the shell exists
  3. Set root namespaces - updates each project's <RootNamespace> in the .csproj
  4. Add to solution - adds new projects to the .sln file with correct solution folders
  5. Flush references (optional) - removes all existing Carrot.* project references before re-adding, ensuring a clean state
  6. Add references - wires up DependsOnProjects and AdditionalReferences (supporting {repo} path prefix for repo-relative resolution). Under implement an edge that leaves the selected set — onto a Generated/Architect project on the brand feed — is wired as a floating <PackageReference Version="*"/> instead of a <ProjectReference>; in-set edges, and every edge under build/full, stay project references

DotNet CLI Wrapper

DotNet wraps Carrot.VisualStudio.Projects and Carrot.VisualStudio.Solution - all operations respect DryRun mode.

Dependencies

DependencyKind
Carrot.Data.Scaffold.Compilersproject (compiler pipeline framework)
Carrot.VisualStudioproject (VS project/solution manipulation)

Build

bash
dotnet build

Usage Guide

Pipeline action that ensures the .NET project structure matches the scaffold graph - creates projects, sets namespaces, manages solution membership and references.

Getting Started

csharp
using Carrot.Data.Scaffold;
using Carrot.Data.Scaffold.Domain;

scaffold.Compiler(compiler =>
{
    compiler
        .AddModule<MyGraphRoot, MyModule>()
        .AddActionEnsureProjects(options =>
        {
            options.Enabled        = true;
            options.UpdateSolution = true;
        })
        .SinkToFileSystem()
        .Build();
});

Common Patterns

1. Basic registration (extension method)

csharp
// No options - registers at AfterModules with defaults
compiler.AddActionEnsureProjects();

// With options
compiler.AddActionEnsureProjects(options =>
{
    options.Enabled        = true;
    options.UpdateSolution = true;
    options.LogStructure   = true;
});

2. Full options

csharp
compiler.AddActionEnsureProjects(options =>
{
    options.Enabled         = true;   // master switch
    options.UpdateSolution  = true;   // create projects, set namespaces, add to sln, wire refs
    options.FlushReferences = true;   // remove all existing Carrot.* refs before re-adding
    options.FlushFiles      = false;  // reserved
    options.LogStructure    = true;   // log the full project dependency graph
    options.DryRun          = false;  // if true, no dotnet CLI calls are made
});

3. Manual registration (without extension)

csharp
compiler.AddAction<ScaffoldCompilerActionEnsureProjects, ScaffoldCompilerActionEnsureProjectsOptions>(
    ScaffoldCompilerPipelineEvent.AfterModules,
    options =>
    {
        options.Enabled        = true;
        options.UpdateSolution = true;
    });

Tips / Gotchas

  • Runs at AfterModules - the extension method hardcodes ScaffoldCompilerPipelineEvent.AfterModules. If you need a different event, use the manual AddAction overload.
  • Template must exist - the action auto-installs the Carrot.ClassLib VS template from .templates/VSProjects/Carrot.ClassLib. If the template path doesn't exist, installation will fail.
  • FlushReferences is destructive - it removes all Carrot.* project references before re-adding them. This ensures a clean state but means any manually-added Carrot references will be removed.
  • {repo} prefix in AdditionalReferences - paths starting with {repo} resolve against the CommonRoot option, NOT a hard-coded git-root anymore (the old FileSystem.ExecutingAssemblyDirectoryGitRoot breaks when EnsureProjects runs from a globally-installed scaffold tool). Resolution:
    • CommonRoot unset → every {repo}/... reference becomes a floating <PackageReference Include="<last-segment>" Version="*-*"/> (the brand-feed flow for architecture repos).
    • CommonRoot set AND resolves inside the architecture repo AND the target csproj exists → <ProjectReference> against the resolved path (in-repo common libs).
    • Anything else (CommonRoot set but resolves outside the repo, or the csproj is missing) → falls back to PackageReference.
  • DryRun affects all CLI operations - when DryRun is true, no projects are created, no references are added, and no solution modifications are made. The project graph is still computed and can be logged.

Carrot