Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .agents/agents/source-generator-framework-writer.agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
name: Source Generator Framework Writer
description: "Specialist for Purview.SourceGeneratorFramework generation code using CodeWriter and XmlCodeWriter-style XML doc extensions; ideal for creating or refactoring generator emitters."
tools:
[
"search/codebase",
"edit/editFiles",
"search",
"execute/getTerminalOutput",
"execute/runInTerminal",
"read/terminalLastCommand",
"read/terminalSelection",
"execute/createAndRunTask",
"execute/runTask",
"read/getTaskOutput",
"vscodeTasks/createAndRunTask",
"vscodeTasks/getTaskOutput",
"vscodeTasks/runTask",
]
---

You are a specialist for `Purview.SourceGeneratorFramework` emitter authoring.

## Primary objective

Produce clear, deterministic, maintainable source-generator emission code using `CodeWriter` and XML extension helpers from `XmlCommentWriter`.

## Background knowledge

Before changing any source generator, analyser, or CodeWriter-related code, load and apply the `source-generator-codewriter-modernization` skill. It contains the full source-generator, analyser, and CodeWriter best-practices guidance for this framework, including incremental pipeline design, value equality, deterministic output, and Roslyn version compatibility.

The most important rules are:

- **Analyser for validation; generator for generation.**
- **Syntax for syntax, symbols for declarations, operations for executable semantics.**
- **Use `ForAttributeWithMetadataName` whenever possible.**
- **Remove `ISymbol`, `Compilation`, `SemanticModel`, `IOperation`, `SyntaxTree`, `SyntaxNode`, and `Location` from incremental pipeline models as early as possible.**
- **Pipeline models must be immutable and value-equatable; use `EquatableArray<T>` for collections.**
- **Avoid `Collect()` until global knowledge is genuinely required.**
- **Never combine `CompilationProvider` into the pipeline merely because it is convenient.**
- **Generate deterministic output and stable hint names.**
- **Test incrementally, not just generated text.**
- **Compile against the oldest Roslyn API version containing the functionality you need.**
- **Create `CodeWriter` inside the output callback and pass it to helpers within that callback; never create it earlier in the pipeline or store it in incremental provider state or custom contexts.**

## Available resources

- `skills/source-generator-codewriter-modernization/SKILL.md` — source-generator, analyser, and CodeWriter best practices for this framework.
- `prompts/refactor-source-generator-to-codewriter.prompt.md` — prompt template for legacy-emitter refactor tasks.

## Must-follow rules

1. Load and apply the `source-generator-codewriter-modernization` skill.
2. Prefer structured declaration APIs over handwritten declaration strings.
3. Prefer XML helper extensions (`XmlSummary`, `XmlParam`, etc.) over raw `///` output.
4. Create `CodeWriter` inside each output callback; never create it earlier in the pipeline or cache it in incremental provider state or custom contexts.
5. Preserve semantic behavior while modernizing implementation style.
6. Keep edits minimal and localized to emitter concerns.

## Refactoring posture

When modernizing legacy code:

- Replace manual indentation/braces with scope APIs.
- Replace signature text with declaration option records.
- Replace ad-hoc XML tags with helper APIs.
- Preserve diagnostics and emitted symbol names.

## Quality gates

- Build/tests pass for impacted projects.
- No scope leaks when materializing generated source.
- Generated artifacts remain deterministic and reviewable.

## Skill routing

When relevant, first load and apply:

- `source-generator-codewriter-modernization`
52 changes: 52 additions & 0 deletions .agents/prompts/refactor-source-generator-to-codewriter.prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
agent: ask
description: "Refactor a legacy source generator emitter from string/StringBuilder to CodeWriter + XmlCodeWriter-style XML extensions with behavior parity."
---

You are modernizing a source generator implementation in this repository. Apply the guidance from the `source-generator-codewriter-modernization` skill for incremental pipelines, value equality, deterministic output, and CodeWriter scope safety.

## Inputs

- Target file(s): `${input:targetFiles:Path(s) to emitter file(s)}`
- Generator type name: `${input:generatorName:Generator class name}`
- Generator version: `${input:generatorVersion:Version string (for generated attributes/header)}`
- Keep output byte-identical where possible: `${input:preserveFormatting:true|false}`

## Task

Refactor the selected legacy emitter implementation from manual `string` / `StringBuilder` output construction to `CodeWriter` and XML documentation extension helpers from `XmlCommentWriter` (XmlCodeWriter-style API usage).

### Requirements

1. Use structured declaration APIs where applicable:
- `Class/Struct/RecordClass/Interface/Enum`
- `Method`, `Property`, `Field`, `Constructor`
2. Use XML helper extensions instead of raw `///` composition:
- `XmlSummary`, `XmlParam`, `XmlReturn`, `XmlRemarks`, `XmlCode` or `XmlCodeBlock`
3. Use `TypeReference` when type text becomes complex (nullability, generics, arrays).
4. Ensure writer lifetime is output-scoped (`generationContext.CreateCodeWriter()` inside callback).
5. Preserve behavior, diagnostics, and generated names.
6. Keep changes minimal and focused; do not reformat unrelated logic.

### Migration strategy

- Identify emitter phases: header, namespace, type declarations, member declarations.
- Replace indentation/braces with scoped APIs.
- Replace signature strings with declaration options.
- Replace XML comments with XmlCommentWriter extension methods.
- Keep semantic equivalence; call out any intentional deltas.

### Verification

- Run relevant tests.
- Confirm generated files still compile.
- Confirm no `CodeWriter` scope leaks (`OpenScopeCount == 0` when materialized).

### Output format

Return:

1. Files changed
2. Why each change was necessary
3. Risks/behavior differences (if any)
4. Verification performed
6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ All `just` recipes read the repository's [`Justfile`](Justfile). Configuration d
| `just test` | Runs the test solution (Debug) with a tree-node filter. |
| `just build-s` | Builds `samples/SampleApp/SampleApp.slnx`. |
| `just test-s` | Runs the sample test solution. |
| `just build-mp` | Builds `samples/SampleApp.MultiProject/SampleApp.MultiProject.slnx`. |
| `just run-mp` | Runs the multi-project sample, printing the aggregated telemetry names. |
| `just clean` | Cleans the main solution. |
| `just restore` | Restores packages for the main solution. |
| `just scrub` | Removes `bin`/`obj` folders, cleans, restores, and shuts down build servers. |
Expand All @@ -62,7 +64,7 @@ conventional-commits check.
| `just update-version` | Runs `.build/update-version.ts` to sync the version into docs/samples. |
| `just pack` | Updates the version then packs the NuGet package into `artifacts/`. |

The version lives in `package.json`. **Current Version:** 5.0.0 — applied to `Version` /
The version lives in `package.json`. **Current Version:** 5.0.2 — applied to `Version` /
`PackageVersion` via the SDK's package.json version detection.

### Pipelines (reusable `purview-build` tool)
Expand Down Expand Up @@ -114,6 +116,8 @@ src/
samples/
├── SampleApp/ # .NET Aspire demo (AppHost, APIService, Web, Shared, ServiceDefaults, UnitTests)
│ └── SampleApp.slnx
├── SampleApp.MultiProject/ # .NET 10 console demo of TelemetryNames aggregation across projects
│ └── SampleApp.MultiProject.slnx # (Observability, Notifications, Payments, Ordering, ConsoleApp)
└── SampleApp.Net48/ # .NET Framework 4.8 console demo
└── SampleApp.Net48.slnx

Expand Down
7 changes: 7 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@
</ItemGroup>
<ItemGroup Label="Samples">
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="$(DotnetRuntimeVersion)" />
<PackageVersion
Include="Microsoft.Extensions.DependencyInjection.Abstractions"
Version="$(DotnetRuntimeVersion)"
/>
<PackageVersion Include="Microsoft.Extensions.Diagnostics" Version="$(DotnetRuntimeVersion)" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.Abstractions" Version="$(DotnetRuntimeVersion)" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="$(DotnetRuntimeVersion)" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="$(MSExtensionVersion)" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="$(MSExtensionVersion)" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="$(OpenTelemetryVersion)" />
Expand Down
23 changes: 22 additions & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ pipeline_feed := "https://api.nuget.org/v3/index.json"
pipeline_tool := ".tools/purview-build/purview-build"

sample_solution_file := "./samples/SampleApp/SampleApp.slnx"
multiproject_sample_solution_file := "./samples/SampleApp.MultiProject/SampleApp.MultiProject.slnx"
multiproject_sample_project := "./samples/SampleApp.MultiProject/SampleApp.MultiProject.ConsoleApp"
artifact_folder := "./artifacts/"
benchmark_solution := "./benchmarks/Purview.Telemetry.Benchmarks/Purview.Telemetry.Benchmarks.csproj"

Expand Down Expand Up @@ -135,6 +137,20 @@ test-s:
echo "Running tests for {{ BLUE }}{{ sample_solution_file }}{{ NORMAL }} with {{ YELLOW }}{{ build_configuration }}{{ NORMAL }}..."
dotnet test --solution "{{ sample_solution_file }}" --configuration "{{ build_configuration }}"

# Builds the multi-project sample solution with the specified configuration

[group('Samples - Build and Test')]
build-mp:
echo "Building {{ BLUE }}{{ multiproject_sample_solution_file }}{{ NORMAL }} with {{ YELLOW }}{{ build_configuration }}{{ NORMAL }}..."
dotnet build "{{ multiproject_sample_solution_file }}" --configuration "{{ build_configuration }}"

# Runs the multi-project sample, printing the aggregated telemetry names

[group('Samples - Build and Test')]
run-mp:
echo "Running {{ BLUE }}{{ multiproject_sample_project }}{{ NORMAL }} with {{ YELLOW }}{{ build_configuration }}{{ NORMAL }}..."
dotnet run --project "{{ multiproject_sample_project }}" --configuration "{{ build_configuration }}"

# -----------------------------------------------------------------------------
# Formatting
# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -188,12 +204,17 @@ code:
code "{{ root_folder }}"

# Opens the sample solution in the default associated application

[group('Utilities')]
vs-s:
echo "Opening {{ BLUE }}{{ sample_solution_file }}{{ NORMAL }}..."
open "{{ sample_solution_file }}"

# Opens the multiproject sample solution in the default associated application
[group('Utilities')]
vs-mp:
echo "Opening {{ BLUE }}{{ multiproject_sample_solution_file }}{{ NORMAL }}..."
open "{{ multiproject_sample_solution_file }}"

# Clean up the repository by removing build artifacts, bin/obj folders etc, and shutting down the build server
[group('Utilities')]
scrub:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Generates [`ActivitySource`](https://learn.microsoft.com/en-us/dotnet/api/system
Add to your `Directory.Build.props` or `.csproj` file:

```xml
<PackageReference Include="Purview.Telemetry.SourceGenerator" Version="5.0.0">
<PackageReference Include="Purview.Telemetry.SourceGenerator" Version="5.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>analyzers</IncludeAssets>
</PackageReference>
Expand Down
2 changes: 1 addition & 1 deletion docs/release-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Merging to `main` triggers `release.yml`, which runs the reusable `purview-relea

## Versioning

- The version lives in `package.json`. **Current Version:** 5.0.0
- The version lives in `package.json`. **Current Version:** 5.0.2
- It is applied to `Version` / `PackageVersion` by `Purview.BuildSdk` via package.json
version detection (`UsePackageJsonVersion`, default `true`).
- `just version` prints the current version.
Expand Down
17 changes: 17 additions & 0 deletions docs/wiki/Breaking-Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This page documents breaking changes between major versions to help you migrate
- [v3 to v4](#v3-to-v4)
- [Namespace consolidation](#namespace-consolidation)
- [OpenTelemetry-aligned naming](#opentelemetry-aligned-naming)
- [v5.0.1 to v5.0.2](#v501-to-v502)
- [v4 to v5](#v4-to-v5)
- [v1 and v2 to v3](#v1-and-v2-to-v3)

Expand Down Expand Up @@ -131,6 +132,22 @@ public enum NamingConvention
> [!TIP]
> For high-impact scenarios, consider using `NamingConvention.Legacy` initially, then gradually migrating to OpenTelemetry conventions during a planned maintenance window.

## v5.0.1 to v5.0.2

**Impact:** Low — affects projects that consume the generated `TelemetryNames` class

### `TelemetryNames` is now opt-in

The generator now records each assembly's generated activity source and meter names in an assembly-level `[GeneratedTelemetryNames]` attribute (`GenerateTelemetryNamesAttribute`, default `true`), which downstream compilations read to aggregate the names of everything they reference. Because that attribute replaces the per-assembly class as the way names travel, `GenerateTelemetryNamesClass` now defaults to `false`.

If a project uses `TelemetryNames.MeterNames` / `TelemetryNames.ActivitySourceNames`, opt it back in:

```csharp
[assembly: TelemetryGeneration(GenerateTelemetryNamesClass = true)]
```

The class it generates now also includes the names recorded by referenced assemblies, so a host project no longer needs one `TelemetryNames` reference per library. Set `AggregateReferencedTelemetryNames = false` for the previous assembly-only behaviour. See [Generation](Generation.md#telemetry-names).

## v4 to v5

**Impact:** Low to Medium — affects the default meter name
Expand Down
24 changes: 24 additions & 0 deletions docs/wiki/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,30 @@ interface IOrderMetrics

The default `MeterNameGenerationType.DotNet` does not add the meter-name prefix. See [Metrics](Metrics.md#meter-naming).

## Registering names

### How do I register the names from every project in a large solution?

Each assembly records its own generated names in metadata (`[assembly: GeneratedTelemetryNames]`), and the `TelemetryNames` class aggregates them across the reference graph. Opt the project that registers telemetry into the class:

```csharp
[assembly: TelemetryGeneration(GenerateTelemetryNamesClass = true)]
```

```csharp
builder.AddServiceDefaults(TelemetryNames.MeterNames, TelemetryNames.ActivitySourceNames);
```

That project needs no telemetry interfaces of its own — the assembly-level attribute is enough, and the class is filled from what its references recorded.

Libraries need no configuration at all, however many of them there are and however complex their telemetry. The aggregate is built at compile time, so it is safe under trimming and native AOT, and it covers transitive references too.

`samples/SampleApp.MultiProject` is a runnable example: four assemblies own telemetry, two of them only transitively referenced by the host, and one `ServiceDefaults`-style project receives the complete set of names (`just run-mp`).

### Why can't my `ServiceDefaults` project generate the names itself?

Because of the reference direction: an Aspire-style `ServiceDefaults` is referenced *by* the projects that own the telemetry, so their assemblies are not in its reference graph. Generate the class in each host (executable) project and pass the arrays into `ServiceDefaults`, as the [sample application](Sample-Application.md#registering-names-with-aspire) does.

## Multi-targeting

### Can I generate Activities, Logging, AND Metrics from one interface?
Expand Down
18 changes: 14 additions & 4 deletions docs/wiki/Generated-Output.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ interface IEntityStoreTelemetry
}
```

generates one partial `EntityStoreTelemetryCore` class split across Activity, Logging, and Metric files, plus a DI extension class and an assembly-level `TelemetryNames` class.
generates one partial `EntityStoreTelemetryCore` class split across Activity, Logging, and Metric files, plus a DI extension class and an assembly-level `[GeneratedTelemetryNames]` attribute recording the generated names.

> [!NOTE]
> To inspect the generated output in your own project, enable `EmitCompilerGeneratedFiles` and look under `obj/<Configuration>/<tfm>/generated/Purview.Telemetry.SourceGenerator/Purview.Telemetry.SourceGenerator.TelemetrySourceGenerator/`.
Expand Down Expand Up @@ -237,21 +237,31 @@ namespace Microsoft.Extensions.DependencyInjection
}
```

## Recorded names (`SampleApp.APIService.GeneratedTelemetryNames.g.cs`)

```csharp
[assembly: global::Purview.Telemetry.GeneratedTelemetryNamesAttribute(new string[] { "SampleApp.APIService" }, new string[] { "SampleApp.APIService" })]
```

Emitted for every assembly that generates an ActivitySource or a Meter, so downstream assemblies can read the names back out of metadata. Both names default to the assembly name here; an assembly-level `[ActivitySourceGeneration]` or `[MeterGeneration]`, or a name on the interface attribute, overrides them.

## Telemetry names (`SampleApp.APIService.TelemetryNames.g.cs`)

Opt in with `[assembly: TelemetryGeneration(GenerateTelemetryNamesClass = true)]`:

```csharp
namespace SampleApp.APIService
{
public static partial class TelemetryNames
{
public static readonly string[] MeterNames = new string[] { "SampleApp.APIService" };
public static readonly string[] MeterNames = new string[] { "SampleApp.APIService", "SampleApp.Shared" };

public static readonly string[] ActivitySourceNames = new string[] { "sample-weather-app-api" };
public static readonly string[] ActivitySourceNames = new string[] { "SampleApp.APIService", "SampleApp.Shared" };
}
}
```

The assembly-level `[assembly: ActivitySourceGeneration("sample-weather-app-api")]` supplies the ActivitySource name; the meter name defaults to the assembly name.
`SampleApp.Shared` comes from the referenced project of the same name, whose `IWeatherForecastTelemetry` declares an ActivitySource and a Meter: the class aggregates this assembly's names with the names recorded by everything it references. See [Generation](Generation.md#telemetry-names).

## See also

Expand Down
Loading
Loading