Skip to content

MSBuild Integration

Deepstaging hooks into the standard .NET build pipeline through Roslyn source generators, analyzers, and MSBuild files shipped inside the NuGet package. No extra build steps or CLI tools are needed — everything runs as part of dotnet build.

How It Works

When you add a <PackageReference Include="Deepstaging" />, NuGet restores the package and MSBuild automatically imports the bundled .props and .targets files. The Roslyn compiler host then loads the generators and analyzers from the analyzers/ folder inside the package.

dotnet build
  ├─ MSBuild imports Deepstaging.props / Deepstaging.targets
  ├─ Roslyn loads Deepstaging.Generators.dll  (source generators)
  ├─ Roslyn loads Deepstaging.Analyzers.dll   (diagnostic analyzers)
  ├─ Roslyn loads Deepstaging.CodeFixes.dll   (IDE code fixes)
  └─ Generators emit source → compilation continues with generated code

Source generators run as part of the compilation itself — they see your syntax trees, produce new source files, and feed them back into the same compilation pass. Analyzers run in parallel, reporting diagnostics that surface as build warnings/errors.

NuGet Package Structure

The Deepstaging package is a metapackage that bundles everything consumers need:

Deepstaging.nupkg
├── analyzers/dotnet/cs/
│   ├── Deepstaging.Generators.dll         ← source generators
│   ├── Deepstaging.Analyzers.dll          ← diagnostic analyzers
│   ├── Deepstaging.CodeFixes.dll          ← IDE code fix providers
│   ├── Deepstaging.Projection.dll         ← shared models (generator dependency)
│   └── Deepstaging.Roslyn.dll             ← Roslyn utilities
├── satellite/netstandard2.0/
│   └── Deepstaging.Projection.dll         ← opt-in reference for downstream generators
└── build/
    ├── Deepstaging.props                  ← imported early (properties)
    └── Deepstaging.targets                ← imported late (items, file nesting)

The analyzers/ folder is a well-known NuGet convention — DLLs placed here are automatically loaded by the Roslyn compiler host without appearing as compile references in your project.

Runtime dependencies (Deepstaging.Abstractions, Deepstaging.Runtime) are delivered as standard lib/ references via package dependencies.

Shipped MSBuild Files

Deepstaging.props (runs early)

Sets compiler-visible properties and configures generated file output:

<PropertyGroup>
    <EmitCompilerGeneratedFiles>false</EmitCompilerGeneratedFiles>
    <CompilerGeneratedFilesOutputPath>!generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

<ItemGroup>
    <CompilerVisibleProperty Include="DeepstagingPreludeLanguageExt"/>
</ItemGroup>

This also handles satellite projection opt-in when DeepstagingSatellite is set.

Deepstaging.targets (runs late)

Handles file discovery and IDE nesting for the Configuration module:

  • Adds **/*.schema.json as AdditionalFiles so analyzers can detect and validate schemas
  • Copies deepstaging.settings*.json to the output directory
  • Nests config files under deepstaging.settings.json and schema files under deepstaging.schema.json for tidy IDE display

Directory.Build.props

The repo uses a modular Directory.Build.props that imports shared build configuration:

<Import Project="build/Build.Language.props"/>    <!-- LangVersion, Nullable, TreatWarningsAsErrors -->
<Import Project="build/Build.Paths.props"/>       <!-- Package output paths -->
<Import Project="build/Build.SourceLink.props"/>  <!-- SourceLink, embedded symbols -->
<Import Project="build/Build.Generators.props"/>  <!-- EmitCompilerGeneratedFiles → obj/Generated/ -->
<Import Project="build/Build.Packaging.props"/>   <!-- NuGet metadata, versioning -->

Key properties set by these imports:

Property Value Effect
LangVersion latest C# 14 features including extension members
Nullable enable Nullable reference types everywhere
TreatWarningsAsErrors true Warnings are build errors — dotnet build is the lint step
EmitCompilerGeneratedFiles true Generated source written to obj/Generated/ for inspection
CompilerGeneratedFilesOutputPath obj/Generated Keeps generated files out of source globbing

Local Development Overrides

Copy Directory.Build.Dev.props.template to Directory.Build.Dev.props (gitignored) for local customizations. This file is only imported during Debug builds:

<Import Project="Directory.Build.Dev.props"
        Condition="Exists('...') AND '$(Configuration)' == 'Debug'"/>

Build Properties

Compiler-Visible Properties

Deepstaging uses <CompilerVisibleProperty> items to pass MSBuild properties into the Roslyn analyzer/generator context. Analyzers read these from AnalyzerConfigOptionsProvider.GlobalOptions:

Property Purpose
DeepstagingPreludeLanguageExt Controls LanguageExt prelude generation
DeepstagingSatellite Enables satellite projection for downstream generators

The TrackedFileTypeAnalyzer base class (from Deepstaging.Roslyn) automatically forwards all MSBuild properties prefixed with Deepstaging into Diagnostic.Properties, so code fixes can read them without per-property wiring.

Prelude Generator

The PreludeGenerator automatically emits a Deepstaging.GlobalUsings.g.cs file with global using directives for common namespaces:

  • Deepstaging.Effects
  • LanguageExt (unless DeepstagingPreludeLanguageExt is false)
  • System.Collections.Immutable
  • System.Threading.Tasks
  • System.Linq

To suppress the LanguageExt global using (useful when it conflicts with test frameworks):

<PropertyGroup>
    <DeepstagingPreludeLanguageExt>false</DeepstagingPreludeLanguageExt>
</PropertyGroup>

Controlling Generator Output

In consumer projects, the NuGet .props sets CompilerGeneratedFilesOutputPath to !generated. During repo development, the workspace-level Build.Generators.props overrides this to obj/Generated/ to keep generated files out of version control.

Suppressing Diagnostics

All Deepstaging diagnostics can be suppressed using standard .NET mechanisms:

<PropertyGroup>
    <NoWarn>$(NoWarn);DSEFX03;DSID02</NoWarn>
</PropertyGroup>
[*.cs]
dotnet_diagnostic.DSEFX03.severity = none
dotnet_diagnostic.DSID02.severity = suggestion
#pragma warning disable DSEFX03
[EffectsModule(typeof(ConcreteService))]
public sealed partial class ServiceEffects;
#pragma warning restore DSEFX03

Suppressing vs fixing

Before suppressing a diagnostic, check the diagnostics reference — many diagnostics have automated code fixes available in your IDE.

IDE Integration

Visual Studio

Generators and analyzers load automatically. Generated files appear under Dependencies → Analyzers → Deepstaging.Generators in Solution Explorer. Code fixes appear in the lightbulb menu.

JetBrains Rider

Rider supports source generators and Roslyn analyzers natively. Generated files are visible under Dependencies → Source Generators. Run Build → Rebuild Solution if generated code is not immediately visible after adding attributes.

VS Code (C# Dev Kit)

The C# extension loads analyzers and generators via OmniSharp or the C# Dev Kit language server. Diagnostics appear in the Problems panel; code fixes are available via Quick Fix (Ctrl+.).

Generated file visibility

Generated files are written to obj/Generated/ (or !generated/ in consumer projects). They are not checked into source control but can be inspected for debugging.

Debugging Generators

Prefer tests over debugger-attach

Deepstaging generators have comprehensive test coverage using snapshot testing and the RoslynTestBase harness. Writing a failing test that reproduces the issue is almost always faster and more reliable than attaching a debugger to the compiler host. See Testing for the test patterns.

If you still need to step through a generator interactively:

  1. Inspect generated output — check obj/Generated/Deepstaging.Generators/ for the emitted .cs files. Most issues are visible from the output alone.

  2. Attach a debugger as a last resort — add a launch point in the generator's Initialize method:

    #if DEBUG
    if (!System.Diagnostics.Debugger.IsAttached)
        System.Diagnostics.Debugger.Launch();
    #endif
    

    Build the project and the JIT debugger dialog will appear.

Debugger.Launch() is disruptive

The debugger dialog fires on every build, including IDE background compilations. Remove the launch point as soon as you're done — or guard it behind an environment variable.

Satellite Projection

Deepstaging exposes its Projection layer — the models, query extensions, and attribute wrappers used by generators — to downstream satellite packages. This lets projects like Deepstaging.Web build upon DispatchModel, EffectsModuleModel, etc. without duplicating discovery logic.

Opting In

The Deepstaging NuGet package bundles Deepstaging.Projection.dll in a satellite/ folder (separate from the analyzers/ copy used by the core generators). Downstream projects opt in via a single MSBuild property:

<PropertyGroup>
    <DeepstagingSatellite>true</DeepstagingSatellite>
</PropertyGroup>

This adds Deepstaging.Projection.dll as a compile reference, giving the project access to all projection types.

Convention: {PackageNameNoDots}Satellite

Every Deepstaging-family package follows this naming convention:

Package Property Exposes
Deepstaging DeepstagingSatellite Deepstaging.Projection.dll
Deepstaging.Web DeepstagingWebSatellite Deepstaging.Web.Projection.dll

A downstream project can compose multiple satellite references:

<PropertyGroup>
    <DeepstagingSatellite>true</DeepstagingSatellite>
    <DeepstagingWebSatellite>true</DeepstagingWebSatellite>
</PropertyGroup>

Why Not lib/?

Putting Deepstaging.Projection.dll in lib/netstandard2.0/ would expose Roslyn symbol wrappers and query extensions to all consumers via IntelliSense — types that only make sense for generator authors. The satellite/ folder keeps them invisible to regular consumers while making them available on demand.

Creating a Satellite Package

If you're building a package that others should be able to extend:

  1. Ship build props — create build/{PackageId}.props that checks for the satellite property:

    <ItemGroup Condition="'$(MyPackageSatellite)' == 'true'">
        <Reference Include="MyPackage.Projection"
                   HintPath="$(MSBuildThisFileDirectory)../satellite/netstandard2.0/MyPackage.Projection.dll"/>
    </ItemGroup>
    
  2. Bundle Projection in satellite/ — add to your packable .csproj:

    <None Include="path/to/MyPackage.Projection.dll"
          Pack="true" PackagePath="satellite/netstandard2.0" Visible="false"/>
    
  3. Suppress NU5100 — NuGet warns about DLLs outside lib/:

    <NoWarn>$(NoWarn);NU5100</NoWarn>
    

The roslynkit template generates this structure automatically.