cai.analyzers
0.30.0
dotnet add package cai.analyzers --version 0.30.0
NuGet\Install-Package cai.analyzers -Version 0.30.0
<PackageReference Include="cai.analyzers" Version="0.30.0" />
<PackageVersion Include="cai.analyzers" Version="0.30.0" />
<PackageReference Include="cai.analyzers" />
paket add cai.analyzers --version 0.30.0
#r "nuget: cai.analyzers, 0.30.0"
#:package cai.analyzers@0.30.0
#addin nuget:?package=cai.analyzers&version=0.30.0
#tool nuget:?package=cai.analyzers&version=0.30.0
About
This has analyzer for CAI apis
Steps
Create a new asp.net web api project
Add HotChocolate.AspNetCore > 16.0.3, HotChocolate.Data >= 16.0.3, Wolverine > 3.4.0 and Cai.Abstraction
Setup HotChocolate and Wolverine as needed. Two helper functions have been provided - builder.addCai(...) and app.useCai(...)
Apply the attributes Rest, Query and Mutation as needed.
Apply RemoteHandler or Distributed to use Nats as the message broker in addition to Wolverine.
Apply AutoLog to log all requests.
Nats.Net#2.5.5 required
Known Issues
Due to a limitation with HotChocolate, ensure that there is at least 1 query for graphql to work. This will be addressed in a future update
Usage
Add the package with the following properties to cai.analyzers: OutputItemType="Analyzer" ReferenceOutputAssembly="false"
Breaking Changes
v0.20.0 is a breaking change.
- Nats.Net v2.5.5 required
- OpenTelemetry is required
Sample packages
<ItemGroup>
<PackageReference Include="HotChocolate.AspNetCore" Version="16.0.3" />
<PackageReference Include="HotChocolate.Data" Version="16.0.3" />
<PackageReference Include="NATS.Net" Version="2.8.0" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.15.3" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.7.0-rc.1" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.2" />
<PackageReference Include="OpenTelemetry.Instrumentation.EntityFrameworkCore" Version="1.10.0-beta.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.1" />
<PackageReference Include="Ulid" Version="1.4.1" />
<PackageReference Include="WolverineFx" Version="5.39.2" />
</ItemGroup>
Require configuration in app.config
Add the configuration below to your app.config. This is required by
"otel": {
"serviceName": "[change me]",
"activitySourceName": "[change me]",
"logs": [
{
"url": "http://localhost:4317",
"protocol": "Grpc"
},
{
"url": "console"
}
],
"traces": [
{
"url": "http://localhost:4317",
"protocol": "Grpc"
},
{
"url": "console"
}
],
"metrics": [
{
"url": "http://localhost:4317",
"protocol": "Grpc"
},
{
"url": "console"
}
],
"tags": [
{
"name": "app.name",
"value": "[change me]"
},
{
"name": "app.version",
"value": "[change me]"
}
]
},
"nats": {
"url": "nats://127.0.0.1:4222",
"username": "u",
"password": "p",
"tlsMode": "Implicit",
"services": {
"defaultNamespace": "DefaultNamespace"
}
}
Nats subject namespaces
When nats.services.defaultNamespace is set and not empty, all subjects are prefixed with it at runtime
(e.g. calc.add becomes CaiTest.calc.add) so that multiple apps can share one NATS server. Both the client
and the server side use NatsService.getSubject(subject) so they always agree. The namespace is sanitized to
NATS-safe subject characters (A-Za-z0-9_- and single . separators; everything else is dropped), so a
misconfigured namespace can never produce an invalid subject.
Per attribute, prefixing can be turned off with prefixNamespace: false (default true) when a subject should
be global:
[Distributed("global.topic", prefixNamespace: false)]
public record GlobalAction(...);
AutoLog
Applying [AutoLog] to a record enables structured request/response logging for all of its Query, Mutation and
Rest endpoints. Request/response payloads are flattened into dotted attribute keys so they stay searchable in Seq
instead of being written as a single value.ToString().
Each operation's payload is namespaced by its GraphQL field/alias path (or, for REST, the verb+path), so
concurrent resolvers on one request don't overwrite each other. E.g. { a: add(...) b: add(...) } produces
a.input.value1, b.input.value1, a.output.*, b.output.*; a single add produces add.input.value1.
The session tag store is thread-safe (ConcurrentDictionary), so parallel HotChocolate resolvers no longer
corrupt it.
[AutoLog("add", maxDepth: 2)]
[Distributed("calc.add")]
[Query<CalculatorResult>]
public record Add(int value1, int value2);
activityName- override the activity/action name (defaults to the endpoint name).logRequest/logResponse- whether the input/output should be written (defaults totrue).disabled- set totrueto disable logging for this record (defaults tofalse).maxDepth- how deep nested objects are flattened:0(default) - use the global default depth,-1- no limit,- any other value - fixed depth.
Global logging limits
The generator cannot read appsettings.json at compile time, so global defaults are provided as MSBuild
properties in the consuming project (or a Directory.Build.props):
<PropertyGroup>
<CaiLoggingMaxDepth>3</CaiLoggingMaxDepth>
<CaiLoggingMaxItems>20</CaiLoggingMaxItems>
<CaiLoggingMaxProperties>50</CaiLoggingMaxProperties>
</PropertyGroup>
These are baked into the generated LogOptions class. Arrays and dictionaries are capped at maxItems,
complex objects at maxProperties, and a .__truncated flag is emitted when a limit is hit. Depth limits
prevent dumping large nested graphs.
ILoggable
Types can take full control of what gets logged by implementing Cai.ILoggable. Return relative dotted keys
with primitive leaf values; the generated Logging helper prefixes them with the section being logged
(e.g. input.order.id):
public record Add(int value1, int value2) : ILoggable
{
public IEnumerable<KeyValuePair<string, object?>> getLogData()
{
yield return new("value1", value1);
yield return new("value2", value2);
}
}
Measures (OpenTelemetry metrics)
Generated end-to-end metrics are exported through the OTLP endpoints configured under otel.metrics
(console/Seq). They flow to the telemetry backend, not to NATS; the NATS message counters are generated
from the service handlers.
Global measures (always on, unless disabled)
| Name | Type | Unit | Attributes |
|---|---|---|---|
cai.request.active |
UpDownCounter | requests | operation, kind, transport, outcome |
cai.request.processed |
Counter | requests | operation, kind, transport, outcome |
cai.request.duration |
Histogram | ms | operation, kind, transport, outcome |
cai.request.errors |
Counter | requests | operation, kind, transport, outcome |
cai.nats.messages.active |
UpDownCounter | messages | service, subject, outcome |
cai.nats.messages.processed |
Counter | messages | service, subject, outcome |
cai.nats.message.duration |
Histogram | ms | service, subject, outcome |
cai.nats.messages.errors |
Counter | messages | service, subject, outcome |
cai.subscriptions.active |
UpDownCounter | streams | outcome |
cai.subscriptions.processed |
Counter | events | outcome |
cai.subscriptions.errors |
Counter | events | outcome |
Per-type measures (only for records marked [Measure])
Apply [Measure] to a record to get dedicated per-type instruments (e.g. "how many Add messages are being
processed"). Without the attribute the record only contributes to the global counts; with it, per-type
instruments are generated and its endpoints/clients record into both.
[Measure]
[Distributed("calc.add")]
public record Add(int value1, int value2);
cai.measure.<type>.active/.processed/.duration/.errors— operations of that type, with ascopeattribute (request= generated client,message= NATS server handler) andoutcome.cai.subscriptions.<type>.active/.processed/.errors— subscription events, in a separate measure family, when the record is a subscription.
Configurability
Runtime switch (appsettings):
otel.metricsEnabled(bool, defaulttrue). Whenfalse, the metrics pipeline is not wired and all instrument calls become near no-ops.Compile-time kill switch (project property, like the logging limits):
<PropertyGroup> <CaiMetricsEnabled>true</CaiMetricsEnabled> <CaiMetricsPrefix>cai</CaiMetricsPrefix> </PropertyGroup>When
CaiMetricsEnabledisfalse, no metrics code is emitted at all (zero overhead).CaiMetricsPrefixchanges the meter/instrument name prefix (defaultcai, e.g.cai.request.active), so you can scope names per project (apitest.request.active).The
cai.analyzerspackage ships abuildTransitive/cai.analyzers.propsthat automatically exposes theCai*properties to the generator (CompilerVisibleProperty), so package consumers only set the property values — no extra items. (ProjectReference-based development setups must add theCompilerVisiblePropertyitems themselves, sincebuildTransitiveassets are not propagated byProjectReference.)Custom meters:
otel.meterslists additional meter names to subscribe:"otel": { "metricsEnabled": true, "meters": ["Cai.Test.Metrics"] }Define your own instruments in the app and they are exported automatically:
public static class TestMetrics { private static readonly Meter meter = new("Cai.Test.Metrics", "1.0.0"); private static readonly Counter<long> adds = meter.CreateCounter<long>("cai.test.adds"); public static void addProcessed() => adds.Add(1); }Instruments are thread-safe and need no DI, so any handler/endpoint can increment them:
TestMetrics.addProcessed();.Do not name an app-level metrics class
AppMetrics— it collides with the generatedApp.Features.Observability.AppMetricsin theApp.*namespace chain.In-code customization:
addCai(...)accepts ametricsOverridesaction (also passed toaddTelemetry) for anythingotel.meterscan't express (views, readers, extraAddMeter, etc.):builder.addCai((opts) => { ... }, (gql) => { ... }, metricsOverrides: m => { m.AddMeter("Some.Other.Meter"); // m.AddView(...); etc. });
Multiple instances (distributed)
Metrics are aggregated per process. Each instance exports its own timeseries tagged with the resource
attributes service.name and service.instance.id (set via otel.serviceInstanceId, falling back to the
HOSTNAME env var, then a generated id). Aggregate views across instances use sum(...) at query time
(e.g. fleet-wide sum(cai_nats_messages_processed)); "active" is per-instance and a global active count is
sum(cai_nats_messages_active) over instances.
Distributed tracing span model
A request that travels over NATS produces two spans in one trace (same traceId):
- the client-side request span (HTTP/GraphQL resolver), and
- the NATS server-handler span, named after the subject (e.g.
calc.add), linked as a child via theactivityStateheader.
If a client span appears named after an arbitrary action (e.g. Add Some Number), it comes from the calling
code setting its own action name (session.setAction(...)), not from the generated handler.
NATS service startup
NATS services start through a generated NatsServicesHost (BackgroundService) with exponential backoff
(1s → 2s → 4s … cap 30s). It pings the connection first and retries with warnings when NATS is unavailable,
so the app starts even if NATS is down and the services register themselves once it is reachable. Errors
starting a service are logged via ILogger. The service classes resolve IMessageBus per invocation from a
disposed scope (no scope leak), and empty service versions default to 0.0.1.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.30.0 | 79 | 10/4/2026 |
| 0.22.6 | 95 | 9/20/2026 |
| 0.22.5 | 159 | 6/29/2026 |
| 0.22.4 | 112 | 6/28/2026 |
| 0.22.3 | 120 | 6/28/2026 |
| 0.22.2 | 114 | 6/28/2026 |
| 0.22.1 | 123 | 5/18/2026 |
| 0.22.0 | 112 | 5/18/2026 |
| 0.21.1 | 2,446 | 2/1/2025 |
| 0.21.0 | 209 | 1/10/2025 |
| 0.20.0 | 189 | 1/8/2025 |
| 0.10.6 | 323 | 11/2/2024 |
| 0.10.4 | 387 | 8/5/2024 |
| 0.10.3 | 318 | 3/13/2024 |
| 0.10.2 | 393 | 3/13/2024 |
| 0.10.1 | 222 | 3/13/2024 |
| 0.10.0 | 431 | 1/29/2024 |
| 0.0.9 | 293 | 1/20/2024 |
| 0.0.8 | 201 | 1/19/2024 |
| 0.0.7 | 221 | 1/12/2024 |