mcpbeat Sign in

Csharp Dotnet Agent Skill

Use when writing, reviewing, testing, or shipping C# / .NET code — ASP.NET Core APIs (minimal APIs vs controllers), EF Core data access, async correctness, solution layout in .cs/.csproj/.sln. NOT a Java/Spring backend (that is spring-boot), NOT a Node/TypeScript backend (that is nestjs), NOT framework-neutral REST naming (that is api-design).

8k tokens
context cost
the whole folder, loaded on every use
7
files
ships runnable scripts
0
copies elsewhere
how many repositories repackaged it
105
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/ericrisco/rsc-harness --skill csharp-dotnet

What comes with it

19 371 bytes besides the instruction
evals/README.md
evals/cases.yaml
references/aspnetcore.md
references/async.md
references/efcore.md
scripts/verify.sh

The instruction itself

11 sections, as written by the author

Modern C# and .NET services

You write and review C#, ASP.NET Core APIs, and EF Core data access. You own the

.NET idioms, the HTTP-contract patterns *as expressed in .NET*, async correctness,

and the .NET-specific quality/security gates. You delegate everything language-

agnostic to the right sibling.

**Target: .NET 10 (LTS, released 2025-11-11, supported 3 years) with C# 14, EF Core

10, ASP.NET Core 10.** .NET 9 / C# 13 is the immediate STS predecessor — still in

support but shorter-lived. Features gate on the target framework moniker (TFM): a

net10.0 project gets C# 14 by default; a net9.0 project caps at C# 13. When you

use a C# 14 feature (field, extension members, null-conditional assignment), say

which TFM it requires.

When NOT to use — delegate

| Situation | Route to |

|---|---|

| Java / Spring Boot backend | spring-boot |

| Node / NestJS TypeScript backend | nestjs |

| Language-agnostic threat modeling, OWASP authz/abuse review | ../secure-coding/SKILL.md |

| Raw Postgres schema/index/query tuning | ../postgresdb/SKILL.md |

| Framework-neutral REST resource taxonomy | api-design |

| Dockerfile / CI pipeline / deploy target | ../deployment/SKILL.md |

| Recording per-project conventions in the workspace wiki | ../harness/SKILL.md |

The .NET *expression* of an API contract and async correctness live HERE. Resource

naming theory lives in api-design; the JVM and Node counterparts are spring-boot

and nestjs.

Project & solution layout + dotnet CLI

dotnet new sln -n Shop                         # solution
dotnet new webapi -n Shop.Api                  # minimal API (default in .NET 10)
dotnet new webapi -n Shop.Api --use-controllers # opt into MVC controllers instead
dotnet new xunit -n Shop.Api.Tests             # test project
dotnet sln add Shop.Api Shop.Api.Tests         # wire into the solution
dotnet add Shop.Api package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet ef migrations add Initial -p Shop.Api   # create a migration
dotnet ef database update -p Shop.Api          # apply migrations
dotnet build -warnaserror                      # compile; analyzers + NRT as errors
dotnet test                                    # run tests
dotnet format                                  # apply style; --verify-no-changes in CI
dotnet publish -c Release                       # produce deployable output

Recommended shape:

Shop.sln
Directory.Packages.props    # central package management: <PackageVersion> here, no versions in csproj
Directory.Build.props       # shared <Nullable>enable</Nullable>, <TreatWarningsAsErrors>, LangVersion
src/Shop.Api/               # endpoints grouped by feature module (Products/, Orders/)
src/Shop.Domain/            # entities, value objects (no EF/ASP.NET dependency)
tests/Shop.Api.Tests/

Central package management: turn it on with <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>

and list versions once in Directory.Packages.props. Why: one source of truth, no

version drift across projects.

NRT on for every project — <Nullable>enable</Nullable> in Directory.Build.props.

Why: the compiler turns whole classes of NullReferenceException into build-time

warnings. Never disable it to silence a warning — fix the nullability.

Modern C# idioms (C# 14 / .NET 10)

DTO as a record, not a class:

// Bad: mutable class, no value equality, hand-written boilerplate
public class ProductDto { public int Id { get; set; } public string Name { get; set; } }

// Good: immutable record DTO
public record ProductDto(int Id, string Name, decimal Price);

Why: value equality, with expressions, no boilerplate — and never reuse an EF entity

as the wire DTO (see anti-patterns).

Pattern matching over if-chains:

// Bad
if (shape is Circle) { var c = (Circle)shape; return Math.PI * c.R * c.R; }

// Good
return shape switch
{
    Circle c      => Math.PI * c.R * c.R,
    Rectangle r   => r.W * r.H,
    _             => throw new ArgumentOutOfRangeException(nameof(shape)),
};

field keyword — field-backed property without a hand-written backing field

(requires C# 14 / net10.0):

// Bad: explicit backing field just to trim a string
private string _name = "";
public string Name { get => _name; set => _name = value.Trim(); }

// Good (C# 14)
public string Name { get; set => field = value.Trim(); }

Primary constructors, required/init, collection expressions:

public class OrderService(IOrderRepository repo, ILogger<OrderService> log)  // primary ctor: DI in one line
{
    public required string Region { get; init; }                            // must be set at construction, then immutable
    private static readonly int[] DefaultTiers = [1, 2, 3];                  // collection expression
}

Prefer NRT annotations over defensive null checks: declare string? note when null is

valid and let the compiler force callers to handle it, instead of if (x == null)

guards scattered everywhere.

Dispose deterministically: using / await using (or using declarations) for

anything IDisposable/IAsyncDisposable. Why: leaked connections and handles.

async/await correctness

This is the highest-leverage area to get right. Core rules in body; the full catalog

(ConfigureAwait, ValueTask, IAsyncEnumerable, Channels, parallelism, deadlock cases)

is in references/async.md — read it before any non-trivial async review.

  • No async void except top-level event handlers. Why: exceptions escape onto the

thread pool and crash the process; the caller cannot await or catch it. Use async Task.

  • Never .Result / .Wait() / GetAwaiter().GetResult() on the request path. This

is the classic deadlock and the answer to "my endpoint hangs under load":

// Bad: blocks the thread on an async call -> thread-pool starvation / deadlock under load
public IActionResult Get() => Ok(_svc.LoadAsync().Result);

// Good: await all the way up
public async Task<IActionResult> Get(CancellationToken ct) => Ok(await _svc.LoadAsync(ct));
  • Flow CancellationToken from the endpoint down into every async call —

SaveChangesAsync, queries, outbound HTTP. Why: a dropped token means the request

keeps running after the client gives up.

  • ValueTask only for hot paths that usually complete synchronously; default to

Task. Never await a ValueTask twice.

  • IAsyncEnumerable<T> for streaming results instead of materializing a huge list.

ASP.NET Core APIs

Decision: minimal API for new services; controllers only when you need MVC

machinery (model binding conventions, action filters at scale, views). Both can coexist.

Why minimal by default: less ceremony, first-class in .NET 10.

Production minimal API shape — route group + DataAnnotations validation (built in for

minimal API parameters in .NET 10) + ProblemDetails + OpenAPI 3.1:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();                 // OpenAPI 3.1, JSON Schema 2020-12
builder.Services.AddProblemDetails();          // RFC 9457 error bodies
builder.Services.AddDbContext<ShopDb>(o => o.UseNpgsql(builder.Configuration.GetConnectionString("Shop")));

var app = builder.Build();
app.MapOpenApi();                              // serves the OpenAPI document
app.UseExceptionHandler();                     // emits ProblemDetails on unhandled errors

var products = app.MapGroup("/products").WithTags("Products");
products.MapGet("/{id:int}", async (int id, ShopDb db, CancellationToken ct) =>
    await db.Products.AsNoTracking()
        .Where(p => p.Id == id)
        .Select(p => new ProductDto(p.Id, p.Name, p.Price))   // project to DTO, no entity leak
        .FirstOrDefaultAsync(ct) is { } dto
        ? Results.Ok(dto)
        : Results.NotFound());
products.MapPost("/", async (CreateProduct req, ShopDb db, CancellationToken ct) =>
{   // DataAnnotations on CreateProduct are validated automatically in .NET 10
    var entity = new Product { Name = req.Name, Price = req.Price };
    db.Products.Add(entity);
    await db.SaveChangesAsync(ct);
    return Results.Created($"/products/{entity.Id}", new ProductDto(entity.Id, entity.Name, entity.Price));
});
app.Run();

DI lifetimes: Singleton (one for the app), Scoped (one per request — DbContext

lives here; it is not thread-safe, so sharing one across requests corrupts change

tracking), Transient (new each resolve). Never inject a Scoped service into a

Singleton (captive dependency — see anti-patterns). Middleware ordering, endpoint

filters, the options pattern, and auth defaults are in references/aspnetcore.md.

EF Core 10

// Bad: tracking + N+1 — loads orders, then one query per order's customer
var orders = await db.Orders.ToListAsync(ct);
foreach (var o in orders) { var name = o.Customer.Name; /* lazy/round-trip per row */ }

// Good: no-tracking read + single query via projection
var rows = await db.Orders.AsNoTracking()
    .Select(o => new OrderRow(o.Id, o.Customer.Name))   // EF translates to one JOIN
    .ToListAsync(ct);
  • AsNoTracking() on every read-only query — skips change-tracking overhead.
  • Kill N+1 with projection (Select) or Include/ThenInclude; use AsSplitQuery()

when an Include cartesian explosion hurts.

  • Migrations: dotnet ef migrations add <Name> then review the generated Up/Down

before database update. Never hand-edit applied migrations.

  • EF Core 10 highlights: native json column type, vector type + VECTOR_DISTANCE()

for embeddings/RAG, LeftJoin/RightJoin LINQ operators, and named query filters

(multiple filters per entity, selectively disabled).

Raw SQL only via parameterized FromSql/ExecuteSql interpolated strings (EF

parameterizes the holes). Query patterns, migration workflow, the EF Core 10 features,

and raw-SQL safety in full are in references/efcore.md. Schema/index tuning is

../postgresdb/SKILL.md.

.NET security controls

Keep only the .NET-specific controls here; threat modeling and OWASP review go to

../secure-coding/SKILL.md.

  • Parameterized data access — EF LINQ / parameterized FromSql / ADO.NET

SqlParameter. Never concatenate user input into SQL. Why: SQL injection.

  • Secretsdotnet user-secrets in dev, Key Vault / environment in prod. Never in

appsettings.json or source.

  • Antiforgery for cookie-auth browser POSTs; Data Protection for cookies/tokens

(configure a persisted key ring across instances).

  • Auth defaults — authenticate then authorize via middleware; prefer policy-based

authorization; ASP.NET Core 10 is secure-by-default but still verify HTTPS redirection

and HSTS in production.

Testing

  • xUnit is the default. Unit-test domain logic with no framework dependency.
  • Integration tests via WebApplicationFactory<Program> — boots the real pipeline

in-memory and lets you HttpClient your endpoints.

  • Testcontainers to run a real Postgres in a container for EF integration tests

instead of the in-memory provider (which lies about relational behavior).

  • Gate everything with scripts/verify.sh (format + build-warnaserror + test + vuln scan).

Anti-patterns

| Anti-pattern | Why it bites | Do instead |

|---|---|---|

| async void (non-event-handler) | Exceptions crash the process; uncatchable by caller | async Task |

| .Result / .Wait() on request path | Deadlock / thread-pool starvation under load | await all the way |

| Scoped service injected into a Singleton | Captive dependency: stale/DbContext reused across requests | Inject IServiceScopeFactory, or make the consumer scoped |

| Reusing an EF entity as the wire DTO | Over-posting, serialization cycles, leaked columns | Separate record DTO + mapping |

| Tracking queries on read-only reads | Wasted memory/CPU on change tracking | AsNoTracking() |

| Lazy navigation in a loop | N+1 round-trips | Projection or Include |

| Missing CancellationToken | Work continues after client disconnects | Flow the token to all I/O |

| catch (Exception) { } swallow | Hides failures; corrupt state continues | Handle specifically or let UseExceptionHandler map it |

| <Nullable>disable</Nullable> to mute a warning | Re-opens the NRE class of bugs | Keep NRT on; fix the nullability |

Project grounding

Record per-project conventions (target TFM, package manager, lint rules, deploy

target) in the workspace wiki via ../harness/SKILL.md, not inline assumptions. Before

calling a C# change done, run scripts/verify.sh from the solution/project root.

How to use it

Copy the folder

Take ericrisco/csharp-dotnet from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.