Skip to content

Use cases

Use cases

28 concrete examples of what the projects do, tagged by who each one speaks to. Every example shows the code involved and what you get back; follow the guide link for the full detail.

0 use cases

Developer Telemetry SourceGenerator

Tracing, logging, and metrics from a single interface

You want OpenTelemetry activities, structured logs, and metrics around an order service, but you do not want to hand-write the ActivitySource, ILogger, and Meter wiring in every class.

csharp
[ActivitySource]
[Logger]
[Meter]
public interface IOrderServiceTelemetry
{
    [Activity]
    [Info]
    [AutoCounter]
    Activity? PlacingOrder(int orderId, [Baggage] string region);
}

What you get: The generator emits the implementation and an AddOrderServiceTelemetry() DI extension; you register it once and inject the interface wherever telemetry is needed.

One interface replaces the ActivitySource, ILogger, and Meter boilerplate you would otherwise repeat in every service.

Read the guide →
Architect Telemetry SourceGenerator

Standardise telemetry names across services

You want consistent OpenTelemetry meter and activity-source names across many services instead of ad-hoc string literals scattered through the codebase.

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

What you get: A generated TelemetryNames static class holds the names, so registration happens once at startup rather than in every service.

Naming conventions are generated, so snake_case tags and hierarchical metrics stay consistent without a review checklist.

Read the guide →
Developer Event Sourcing

An aggregate that persists as events

You want aggregate-based event sourcing without hand-writing the event stream, snapshot, and transaction plumbing.

csharp
[Aggregate]
public partial class OrderAggregate : AggregateBase
{
    public string CustomerId { get; private set; } = default!;
    public decimal Total { get; private set; }

    [Event]
    public partial void CreateOrder(string customerId);

    [Event]
    public partial void AddLineItem(string productId, string productName, int quantity, decimal unitPrice);
}

What you get: The source generator produces the aggregate plumbing; provider packages add stores for SQL Server, PostgreSQL, MongoDB, Azure Storage, Cosmos DB, or in-memory.

One [Aggregate] declaration, and switching storage means swapping a provider package rather than rewriting the domain.

Read the guide →
Architect Event Sourcing

Choose the store late, keep the domain stable

You need to start on one database and move providers later, but you do not want persistence choices to leak into the domain model.

csharp
builder.Services.AddSqlServerEventStore();
builder.Services.AddSqlServerSnapshotQueryableEventStore();

What you get: Domain code depends on provider-agnostic facades, while concrete stores live behind separate packages and registration extensions.

SQL Server, PostgreSQL, MongoDB, Azure Storage, and Cosmos DB are interchangeable at the registration level, so the domain never references a store.

Read the guide →
Developer ZodSharp

Validate input without exceptions or allocations

You need to validate values at an application boundary and prefer a result you can inspect over exception-driven control flow.

csharp
var nameSchema = Z.String().Min(3).Max(50);
var result = nameSchema.Validate("John");

if (result.IsSuccess)
    Console.WriteLine($"Valid name: {result.Value}");

What you get: Validate/SafeParse return a ValidationResult<T> with no exceptions, while Parse still throws ZodException when you want that behaviour.

Validation is zero-allocation on every valid input path, so the performance claim is a property of the code rather than an aspiration.

Read the guide →
Team lead ZodSharp

Return standard ProblemDetails for invalid requests

You want failed validation in ASP.NET Core to surface as standard ProblemDetails payloads rather than a bespoke error shape that clients must learn.

bash
dotnet add package Purview.ZodSharp.AspNetCore

What you get: The AspNetCore adapter converts failed validation results into the standard ProblemDetails / HttpValidationProblemDetails payloads.

One adapter package, and API consumers keep the error contract they already expect.

Read the guide →
Developer Value Objects

A domain scalar without the boilerplate

You want a strong EmailAddress type with normalisation and validation, without hand-writing factories, equality, conversions, and a JSON converter.

csharp
[Scalar]
public readonly partial record struct EmailAddress
{
    public string Value { get; }

    static partial void OnNormalize(ref string value)
        => value = value?.Trim().ToLowerInvariant()!;
}

What you get: The generator adds Create, Hydrate, TryCreate, and Empty, plus equality, comparison, implicit conversions, and a JSON converter.

One declaration replaces the factory, equality, comparison, conversion, and serialization code you would otherwise maintain by hand.

Read the guide →
Developer Value Objects

Map a value object onto an Entity Framework column

You use a value object in your domain and want it to persist transparently instead of writing converters at every mapping site.

csharp
var email = EmailAddress.Create("  [email protected]  ");
// email.Value == "[email protected]"

bool ok = EmailAddress.TryCreate("not-an-email", out _);
// ok == false

What you get: Value objects serialize as their underlying value and map onto Entity Framework JSON columns, with optional Purview.ZodSharp validation.

Persistence and serialization come from the generated type, so mapping code is not duplicated per entity.

Read the guide →
Contributor SourceGenerator Framework

Build an incremental generator on typed foundations

You are writing a non-trivial incremental C# source generator and need typed attribute models, structured code output, and packaging that Roslyn will actually load.

xml
<ProjectReference
  Include="..\MyGenerator\MyGenerator.csproj"
  PrivateAssets="all"
  OutputItemType="Analyzer"
  ReferenceOutputAssembly="false"
/>

What you get: The framework supplies attribute-data models, a structured code writer, a type library, and packaging helpers so the generator stays focused on its own logic.

The Purview generators themselves are built on the framework, so it is exercised by real generators rather than being an untested abstraction.

Read the guide →
Contributor SourceGenerator Framework

Verify incremental behaviour in tests

You need to prove a generator is incremental and produces the code you expect, without assembling a Roslyn driver by hand in every test.

What you get: The testing package provides SourceGeneratorTestRunner<TGenerator>, a framework-agnostic test base, and assertions such as AssertSingleGeneratedSource and AssertNoCompilationErrors.

Step-cache tests and a TUnit integration ship alongside the runner, so incremental regressions surface in CI.

Read the guide →
Developer Results

Return expected failures as values

You call a store that can fail in ways you expect — not found, disabled, already exists — and you do not want a thrown exception or a generic error string to be the only way the caller learns what happened.

csharp
Result<Tenant, TenantError> GetTenant(TenantId tenantId) =>
    _tenants.TryGet(tenantId, out var tenant)
        ? Result<Tenant, TenantError>.Success(tenant)
        : new TenantNotFound(tenantId).AsFailure<Tenant>();

What you get: The caller branches on the error value itself, so TenantNotFound keeps carrying its TenantId instead of being flattened into a message.

The core package has no dependencies: it ships the result type, its factories, and IResultValue without pulling anything else in.

Read the guide →
Developer Results

Generate the failure helper for every union case

You model errors as a C# 15 union and need each case to become the failure side of a result without writing one conversion helper per case by hand.

csharp
[GenerateResult]
public readonly union TenantError(TenantNotFound, TenantDisabled, TenantAlreadyExists);

What you get: The generator emits AsFailure<TValue>() for every case of an annotated union, plus a code fix and a CA1815 suppression, so the declaration needs no pragma.

C# forbids the implicit conversion a bare case value would need — no operators in static classes, no conversion operators in extension members, one user-defined conversion per sequence — which is why the helper is per case.

Read the guide →
Architect Results

Map one error union onto HTTP responses

The same domain error union surfaces from many minimal-API endpoints, and you do not want a hand-written switch deciding the status code at each one.

csharp
builder.Services.AddResultsHttp(options => options
    .Map<TenantNotFound>(error => TypedResults.NotFound())
    .Map<TenantError>(error => TypedResults.Problem(statusCode: StatusCodes.Status409Conflict))
);

app.MapGet("/tenants/{id:int}", (int id) => GetTenant(id)).WithResultsHttp();

What you get: The mapping is declared once at startup, WithResultsHttp() applies it to the endpoints, and a host can replace IResultsHttpMapper when it needs different behaviour.

Mappings resolve in declaration order — any mapping or failure mapper the host declared earlier always wins — so the most specific rule is registered first.

Read the guide →
Team lead Results

Let ZodSharp validation failures become validation problems

Endpoints validate input with ZodSharp, and a failed validation should reach the client as the standard ASP.NET Core validation-problem payload rather than an error shape the API has to document separately.

json
{
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": { "TenantId": ["Required field 'TenantId' is null"] },
  "issues": [{ "code": "missing_field", "path": ["TenantId"], "message": "Required field 'TenantId' is null" }]
}

What you get: A failure that implements IValidationErrorCarrier becomes HttpValidationProblemDetails without the host mapping it, unless a per-code or per-category rule answers that failure first.

The response is ASP.NET Core's own HttpValidationProblemDetails contract, so API clients keep the error shape they already parse.

Read the guide →
Architect Aspire ResourceKit

Typed, testable resource groups in a large AppHost

A large Aspire AppHost accumulates procedural resource wiring and configuration that is hard to test and easy to misconfigure.

csharp
[HostKit]
partial class ShopHostKit;

[ResourceDefinition<Projects.Example_Service>("api")]
partial class ApiResourceKit
{
    protected override IResourceBuilder<ProjectResource> BuildResource(
        IDistributedApplicationBuilder builder)
        => builder.AddProject<Projects.Example_Service>(Name);
}

What you get: Resource definitions become strongly typed, source-generated classes with generated host wiring and typed options.

One [HostKit] plus one [ResourceDefinition] per resource replaces procedural AppHost code, and the analyzer validates the wiring at compile time.

Read the guide →
Team lead Aspire ResourceKit

Configure and toggle resources without editing the AppHost

You want enablement rules and configuration to be declarative and discoverable instead of buried in the AppHost entry point.

What you get: Typed options and enablement toggles are generated for each resource kit, so behaviour is configured rather than coded.

Resource kits can be unit tested with the testing helpers, independent of a running AppHost.

Read the guide →
Developer AspireC4

A live architecture diagram with three lines of code

You already describe your distributed application in Aspire and do not want to maintain a separate architecture diagram that drifts from reality.

csharp
var builder = DistributedApplication.CreateBuilder(args);

builder.AddAspireC4();

builder.Build().Run();

What you get: AspireC4 writes the generated LikeC4 model, starts the LikeC4 sidecar, and refreshes the diagram whenever the Aspire application changes.

The diagram is derived from the running resource graph, so it cannot silently fall out of date the way a hand-drawn one can.

Read the guide →
Architect AspireC4

Validate architectural metadata at compile time

You want tags, kinds, groups, and metadata in your architecture model checked before the application even runs.

What you get: A Roslyn source generator validates the architectural metadata while the AppHost compiles.

Architectural metadata mistakes fail the build instead of surfacing in a diagram nobody reads.

Read the guide →
Team lead Build

One reusable pipeline for every repository

You maintain several repositories that each repeat the same build, test, pack, and release logic, and the copies drift apart.

yaml
# .github/workflows/pr.yml
jobs:
  build:
    uses: purview-dev/build/.github/workflows/purview-build.yml@main
    secrets: inherit

What you get: Consumers reference the reusable workflow or composite action and configure behaviour with purview-build.json instead of owning pipeline source.

Pipeline code lives in one repository; a consuming repository is a short workflow file plus configuration.

Read the guide →
Contributor Build

Dogfood the same pipeline the organisation uses

You want local commands, pull-request CI, and releases to run the exact same tool and conventions, rather than three approximations.

What you get: The pipeline is a Modular Pipelines .NET tool packaged as a pinned dotnet tool, callable from a composite action, reusable workflows, or the local command line.

Purview's own repositories use the published pipeline, so the tool is exercised on every build.

Read the guide →
Team lead Build SDK

Apply repository-wide defaults from one declaration

You want standard .NET project defaults, code-style enforcement, test wiring, and central package management without repeating the same properties in every project.

json
{
  "msbuild-sdks": {
    "Purview.BuildSdk": "1.0.0"
  }
}

What you get: Install the SDK once via global.json and Directory.Build.props; every project beneath the repository root inherits everything automatically.

One versioned SDK declaration replaces per-project property groups, so conventions are upgraded in one place.

Read the guide →
Contributor Build SDK

Bootstrap a new repository

You are starting a repository and want the organisation's conventions in place before the first project exists.

What you get: The SDK can bootstrap a global.json at the repository root when one is missing, then applies project-type detection, analyzers, and test wiring.

Repository bootstrap, analyzers, and testing wiring are documented and generated, not copied between repos by hand.

Read the guide →
Developer Logging Source Generators

Move to the supported successor

You depend on the original interface-based logging generator and want logging, activities, and metrics generated together.

What you get: Telemetry SourceGenerator generates logging alongside Activities and Metrics from the same interfaces, with DI registration and multi-targeting.

This project is archived and no longer developed; the successor covers a superset of its behaviour from one interface.

Developer Containers

The same test on WSL Containers or Docker

Your integration suite needs real infrastructure, but the machines it runs on differ: a Windows developer laptop with WSL Containers and a Linux CI runner with a Docker daemon.

csharp
await using var container = new ContainerBuilder()
    .WithImage("docker.io/library/redis:latest")
    .WithPortBinding(6379, assignRandomHostPort: true)
    .WithWaitStrategy(Wait.ForTcpPort(6379))
    .Build();

await container.StartAsync();
ushort port = container.GetMappedPublicPort(6379);

What you get: The test never names a backend. Automatic selection prefers WSLC where it is usable and falls through to Docker everywhere else; PURVIEW_CONTAINERS_BACKEND pins one (wsl or docker) when a job should fail loudly instead of falling back.

One API, two backends: the same builder, wait strategies, and connection-string helpers run on both Purview.Containers.Wsl and Purview.Containers.Docker with no code change.

Read the guide →
Developer Containers

Zero-config tests that pick a backend themselves

You want real PostgreSQL and Redis in your tests and would rather not choose a container runtime, add a backend package explicitly, or write any registration or environment configuration.

csharp
await using var postgres = new PostgreSqlBuilder().WithDatabase("app").Build();
await postgres.StartAsync();                         // waits for pg_isready

await using var connection = new NpgsqlConnection(postgres.GetConnectionString());
await connection.OpenAsync();

What you get: Add the umbrella Purview.Containers to a plain net10.0 project: each backend package's buildTransitive assets generate the registration in your assembly, so WSLC is used on a Windows machine and Docker everywhere else with no code or configuration.

Each backend package ships buildTransitive assets that generate a module initializer in the consumer's assembly, so no registration line and no environment variable are required; those assets flow transitively through the umbrella.

Read the guide →
Developer Containers

A typed service module instead of a bare image

You need a throwaway PostgreSQL (or Redis, SQL Server, MySQL, RabbitMQ, Azurite, NATS) and do not want to look up its image, port, environment, and readiness probe every time.

csharp
await using var postgres = new PostgreSqlBuilder()
    .WithDatabase("tests")
    .WithPassword("postgres")
    .Build();

await postgres.StartAsync();   // returns once pg_isready succeeds

What you get: The module supplies the default image, port mapping, and readiness wait strategy, so StartAsync() returns once the instance is genuinely ready, and GetConnectionString() points at the mapped host port — the same shape Testcontainers modules use.

Seven service modules ship (PostgreSQL, Redis, SQL Server, MySQL, RabbitMQ, Azurite, NATS), and a module contributes only its image, ports, environment, wait strategy, and connection string — never the shared runtime.

Read the guide →
Architect Containers

One Windows prerequisite, Docker in CI

You want integration tests that use real infrastructure on Windows developer machines and CI agents, without making Docker Desktop a requirement of the minimum Windows toolchain.

csharp
foreach (var backend in await ContainerBackends.ProbeAllAsync())
{
    Console.WriteLine($"{backend.Name}: usable={backend.IsUsable} version={backend.Version}");
}

What you get: The abstractions and modules are portable net10.0, so a platform-neutral project binds the WSLC facade on Windows and behaves as Docker-only everywhere else; ProbeAllAsync() reports exactly which runtime a machine has before a test runs.

Purview.Containers.Wsl ships a portable net10.0 facade and a net10.0-windows10.0.19041.0 implementation, and the repository builds twenty-one throwaway consumer projects to prove the target-framework contract and the PCC0001/PCC0002 guards.

Read the guide →
Contributor Containers

Add a service module

You want to add a module for another service, or fix one of the existing seven, and need to know exactly what a module is allowed to contribute.

What you get: A module supplies only the default image, ports, environment, its own WithXxx configuration, a wait strategy, and connection-string helpers; the generic builder snapshots the accumulated state into an immutable configuration, so modules never duplicate container runtime infrastructure.

Every module references Purview.Containers.Core only — never a backend package — which is what lets one module package run on WSLC or Docker, and each module has its own unit and integration test project to prove itself.

Read the guide →

Looking for the tools themselves?

Browse the project catalogue for the full family, or the documentation portal for the complete guides behind each example.