skills/backend/aspnet-core/dotnet-9/SKILL.md
Version-specific expert for ASP.NET Core on .NET 9 STS (Nov 2024 - May 2026). Covers built-in OpenAPI document generation, HybridCache, MapStaticAssets, SignalR Native AOT support, TypedResults improvements, and OpenAPI analyzer improvements. WHEN: ".NET 9", "net9.0", "dotnet 9", "AddOpenApi", "MapOpenApi", "HybridCache", "MapStaticAssets", "SignalR AOT", "TypedResults.InternalServerError", "ProducesProblem groups", "Scalar API".
npx skillsauth add chrishuffman5/domain-expert backend-aspnet-core-dotnet-9Install this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are a specialist in ASP.NET Core on .NET 9, the Standard-Term Support release (Nov 2024 - May 2026). Licensed under MIT.
For foundational ASP.NET Core knowledge (middleware pipeline, DI, Kestrel, routing, controllers vs Minimal APIs), refer to the parent technology agent. This agent focuses on what is new or changed in .NET 9.
.NET 9 ships first-class OpenAPI generation for both Minimal APIs and controllers via Microsoft.AspNetCore.OpenApi. Swashbuckle is no longer required.
builder.Services.AddOpenApi();
var app = builder.Build();
app.MapOpenApi(); // Serves at /openapi/v1.json
app.MapGet("/hello/{name}", (string name) => $"Hello, {name}!")
.WithName("GetHello")
.WithSummary("Says hello")
.WithDescription("Returns a greeting for the given name");
Key differences from Swashbuckle:
/openapi/v1.json (not /swagger/v1/swagger.json)// NuGet: Scalar.AspNetCore
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference(); // UI at /scalar/v1
}
builder.Services.AddOpenApi("v1");
builder.Services.AddOpenApi("v2", options =>
{
options.AddDocumentTransformer((document, context, ct) =>
{
document.Info.Title = "My API v2";
return Task.CompletedTask;
});
});
app.MapOpenApi("/openapi/{documentName}.json");
app.MapGet("/v1/resource", GetResource).WithGroupName("v1");
app.MapGet("/v2/resource", GetResourceV2).WithGroupName("v2");
builder.Services.AddOpenApi(options =>
{
// Document-level
options.AddDocumentTransformer((doc, ctx, ct) =>
{
doc.Info.Contact = new OpenApiContact
{
Name = "API Support", Email = "[email protected]"
};
return Task.CompletedTask;
});
// Operation-level (e.g., add security requirement)
options.AddOperationTransformer((operation, ctx, ct) =>
{
if (ctx.Description.ActionDescriptor.EndpointMetadata
.OfType<IAuthorizeData>().Any())
{
operation.Security = [new() { ["Bearer"] = [] }];
}
return Task.CompletedTask;
});
// Schema transformer
options.AddSchemaTransformer((schema, ctx, ct) =>
{
if (ctx.JsonTypeInfo.Type == typeof(decimal))
schema.Format = "decimal";
return Task.CompletedTask;
});
});
dotnet add package Microsoft.Extensions.ApiDescription.Server
dotnet build # Outputs openapi/v1.json
<PropertyGroup>
<OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)/openapi</OpenApiDocumentsDirectory>
</PropertyGroup>
Unified caching abstraction combining L1 (in-process IMemoryCache) and L2 (out-of-process IDistributedCache like Redis), with built-in stampede protection.
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="9.0.0" />
builder.Services.AddHybridCache(options =>
{
options.MaximumPayloadBytes = 1024 * 1024; // 1 MB
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(5),
LocalCacheExpiration = TimeSpan.FromMinutes(1)
};
});
// Optional L2 backend
builder.Services.AddStackExchangeRedisCache(options =>
options.Configuration = builder.Configuration.GetConnectionString("Redis"));
public class ProductService(HybridCache cache, IProductRepository repo)
{
public async Task<Product?> GetProductAsync(int id, CancellationToken ct = default)
{
return await cache.GetOrCreateAsync(
$"product:{id}",
async token => await repo.GetByIdAsync(id, token),
cancellationToken: ct);
}
public async Task InvalidateProductAsync(int id)
{
await cache.RemoveAsync($"product:{id}");
}
}
Avoids closure allocation in hot paths:
return await cache.GetOrCreateAsync(
$"product:{id}",
(repo, id), // TState -- avoids closure
static async (state, ct) => await state.repo.GetByIdAsync(state.id, ct),
cancellationToken: ct);
Key advantages over IDistributedCache:
GetOrCreateAsync call (no manual cache miss handling)Drop-in replacement for UseStaticFiles with build-time and publish-time optimization:
app.MapStaticAssets(); // Instead of app.UseStaticFiles()
What it does:
Performance impact (Microsoft benchmarks):
| Asset | Raw | MapStaticAssets | Reduction | |---|---|---|---| | bootstrap.min.css | 163 KB | 17.5 KB | 89.3% | | jquery.min.js | 89.6 KB | 28 KB | 68.7% | | bootstrap.min.js | 78.5 KB | 6 KB | 92.4% |
When to keep UseStaticFiles: For files not known at build time (user uploads, runtime-loaded resources).
Both SignalR client and server support Native AOT compilation in .NET 9:
var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.AddSignalR();
var app = builder.Build();
app.MapHub<ChatHub>("/chat");
app.Run();
[JsonSerializable(typeof(string))]
internal partial class AppJsonSerializerContext : JsonSerializerContext { }
AOT limitations (.NET 9):
| Limitation | Detail |
|---|---|
| Protocol | JSON only (MessagePack not supported) |
| Strongly typed hubs | Not supported with PublishAot (only PublishTrimmed) |
| Return types | Task, Task<T>, ValueTask, ValueTask<T> only |
builder.Services.AddOpenTelemetry()
.WithTracing(tracing =>
{
tracing.AddSource("Microsoft.AspNetCore.SignalR.Server");
});
app.MapGet("/risky", () =>
{
try { return TypedResults.Ok("Success"); }
catch (Exception ex)
{
return TypedResults.InternalServerError($"Error: {ex.Message}");
}
});
Previously only worked on individual endpoints:
var api = app.MapGroup("/api")
.ProducesProblem(StatusCodes.Status500InternalServerError)
.ProducesValidationProblem();
api.MapGet("/products", GetProducts);
api.MapPost("/products", CreateProduct);
var extensions = new List<KeyValuePair<string, object?>>
{
new("correlationId", Guid.NewGuid()),
new("timestamp", DateTime.UtcNow)
};
return TypedResults.Problem("Validation failed", extensions: extensions);
.NET 9 ships improved Roslyn analyzers included automatically with the framework reference:
Route analysis: Detects ambiguous routes, unreachable definitions, parameter name mismatches.
OpenAPI metadata: Warns when [FromForm] parameters lack [Consumes] metadata, flags missing [ProducesResponseType].
Type safety: RouteHandlerAnalyzer validates route parameter types, warns on nullable mismatches.
#pragma warning disable ASP0018 // Suppress specific analyzer
app.MapGet("/items/{id}", GetItemById);
#pragma warning restore ASP0018
| Area | Change |
|---|---|
| OpenAPI | Default endpoint is /openapi/v1.json (not /swagger/v1/swagger.json) |
| OpenAPI | Swagger UI not included -- add Scalar, Redoc, or NSwag UI |
| HybridCache | Preview at launch -- API surface may change |
| MapStaticAssets | Asset fingerprinting changes URLs -- CDN configs may need updating |
| SignalR AOT | Strongly typed hubs not supported with PublishAot |
| TypedResults | InternalServerError is new -- existing Results.StatusCode(500) still works |
net9.0tools
kubectl command-line usage and scripting: kubeconfig and context management, output formats (jsonpath, custom-columns, go-template), all major verbs (get, describe, apply, delete, exec, logs, port-forward, rollout, scale, drain), workload resources, config/storage, networking, RBAC, node management, debugging (CrashLoopBackOff, ImagePullBackOff, OOMKilled), and scripting patterns (dry-run, diff, wait, jq, kustomize). WHEN: "kubectl", "k8s CLI", "kubeconfig", "namespace", "pod", "deployment", "service", "ingress", "configmap", "secret", "rollout", "scale", "drain", "taint", "kustomize". Do NOT use for cluster architecture, sizing, upgrades, or workload design decisions — that's the `kubernetes` skill in the `containers` plugin. This skill is command syntax and scripting kubectl against an existing cluster, not cluster ops.
tools
Bash 5.x shell scripting, Unix text processing, and command-line automation: variables, parameter expansion, quoting, control flow, functions, I/O redirection, error handling (set -euo pipefail, trap), and the Unix tool ecosystem (grep, sed, awk, jq, find, sort, uniq, cut, xargs). Covers process management, networking (curl, ssh, rsync, nc), file locking (flock), parallel execution, and production script patterns. WHEN: "Bash", "bash", "shell", "sh", ".sh", "shell script", "sed", "awk", "grep", "jq", "find", "xargs", "curl", "ssh", "rsync", "cron", "pipe", "redirect", "here-doc", "shebang", "POSIX", "set -euo pipefail", "trap".
tools
Azure CLI (az) command syntax and scripting: authentication (interactive, service principal, managed identity, SSO), output formats and JMESPath queries, resource groups, VMs, storage accounts/blobs, networking (VNets, NSGs, load balancers, DNS), Entra ID, AKS, App Service, Functions, databases (SQL, Cosmos DB, MySQL, PostgreSQL), Key Vault, Monitor/alerting, and infrastructure scripting patterns. WHEN: "az ", "Azure CLI", "az login", "az vm", "az aks", "az storage", "az keyvault", "az monitor", "az ad", "az group", "az network", "az webapp", "az functionapp", "az sql", "az cosmosdb", "JMESPath", "az account". Do NOT use for Azure architecture, landing zones, or multi-subscription strategy — that's the `cloud-platforms` plugin. This skill is about command syntax and scripting the CLI, not deciding what to provision.
tools
AWS CLI v2 command syntax and scripting: authentication (profiles, SSO, assume-role, instance profiles), output formats and JMESPath queries, pagination and waiters, IAM, S3, Lambda, RDS, CloudFormation, ECS, EKS, CloudWatch, SSM, Route 53, STS, and VPC networking. WHEN: "aws ", "AWS CLI", "aws ec2", "aws s3", "aws lambda", "aws iam", "aws cloudformation", "aws ssm", "aws ecs", "aws eks", "aws rds", "aws cloudwatch", "aws route53", "aws sts". Do NOT use for AWS architecture, service selection, multi-account strategy, or FinOps — that's the `cloud-platforms` plugin. This skill is about command syntax and scripting the CLI, not deciding what to provision.