Skip to content
Back to skills

Csharp Dotnet

ASecurity

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).

  • 134 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 2, 2026
ai-agentstypescriptgojavac#bashsqlnodeexpressspringdocker

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 7 files and shows the line behind each finding

Scanned September 2, 2026

npx -y skills add ericrisco/rsc-harness --skill csharp-dotnet --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Csharp Dotnet?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Csharp Dotnet
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/ericrisco-csharp-dotnet/badge)](https://www.skillsdirectory.com/skills/ericrisco-csharp-dotnet)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: csharp-dotnet
description: "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)."
tags: [csharp, dotnet, aspnetcore, efcore, async]
recommends: [postgresdb, secure-coding, deployment]
origin: risco
---

# 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

```bash
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:

```text
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:

```csharp
// 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:

```csharp
// 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**):

```csharp
// 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:

```csharp
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":

```csharp
// 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:

```csharp
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

```csharp
// 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.
- **Secrets** — `dotnet 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.

Files in this skill

  • SKILL.md12.9 KB
  • evals/README.md815 B
  • evals/cases.yaml3.2 KB
  • references/aspnetcore.md4.5 KB
  • references/async.md3.5 KB
  • references/efcore.md3.9 KB
  • scripts/verify.sh3.1 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.