Skip to main content

C# Backend Instructions — Example

This is an example scoped instruction file for C# backend code. It activates when Copilot is working on C# files. The rules are drawn from Defra software development standards (opens in new tab) — the single source of truth for Defra coding practices. Copy it into .github/instructions/csharp-backend.instructions.md in your repository.

Note: .NET is Defra’s secondary stack — Node.js is the primary choice. Use .NET where there is a justified need, such as Microsoft ecosystem integration (Dynamics 365), CPU-intensive backend processing, or large-scale multi-layer services. .NET/C# must only be used for backend services and APIs — do NOT use .NET for frontend applications or user interfaces.

Example file contents

The content below goes into your .github/instructions/csharp-backend.instructions.md file:


---
applyTo: "**/*.cs,**/*.csproj"
---

# C# Backend

## Language and runtime

- Use the latest LTS version of .NET (currently .NET 10)
- Use modern C# features — pattern matching, records for immutable data, nullable reference types
- Enable nullable reference types (`<Nullable>enable</Nullable>` in csproj)
- Enable implicit usings (`<ImplicitUsings>enable</ImplicitUsings>` in csproj)
- Use `async`/`await` for all asynchronous operations — do not use `.Result` or `.Wait()`
- Follow Microsoft's official C# coding conventions for naming, formatting, and style
- Prefer `var` for local variables when the type is obvious from the right-hand side
- Use containerisation with Docker

## Framework

- Use ASP.NET Core Minimal APIs for new services — do not use MVC controllers, Razor Pages, Blazor, or any other .NET UI framework. Per [Defra .NET Standards](https://defra.github.io/software-development-standards/standards/net_standards/){:target="_blank"} (opens in new tab), .NET is for backend services only; frontends must be built with Node.js + Hapi + GOV.UK Frontend (Nunjucks).
- Register endpoints using `MapGet`, `MapPost`, `MapPut`, `MapDelete`
- Use the built-in DI container; do not develop frontends in .NET

### Endpoint structure

Organise endpoints as static extension methods grouped by domain area:

```csharp
public static class ApplicationEndpoints
{
    public static IEndpointRouteBuilder MapApplicationEndpoints(
        this IEndpointRouteBuilder app)
    {
        app.MapGet("/applications/{id}", async (
            string id,
            IApplicationService applicationService) =>
        {
            var application = await applicationService.GetByIdAsync(id);
            return application is not null
                ? Results.Ok(application)
                : Results.NotFound();
        });

        return app;
    }
}
```

## Architecture

- Follow SOLID principles
- Keep endpoint handlers thin — validate input, call a service, return a result
- Use records for DTOs and value objects
- Use the built-in DI container; register all services at startup
- Do not develop frontend applications in .NET — use Node.js with Nunjucks

```
Config/           # Configuration
Endpoints/        # Minimal API endpoint registrations
Models/           # Domain models and DTOs (prefer records)
Services/         # Business logic
Validators/       # FluentValidation validators
Utils/            # Shared utilities
```

## Validation

- Use FluentValidation for request validation
- Register validators in the DI container
- Return structured error responses with field-level detail
- Validate all user input before processing

✅ Validator example:
```csharp
public class CreateApplicationValidator : AbstractValidator<CreateApplicationRequest>
{
    public CreateApplicationValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty()
            .MaximumLength(200);

        RuleFor(x => x.Reference)
            .NotEmpty()
            .Matches(@"^[A-Z]{2}\d{6}$")
            .WithMessage("Reference must be two letters followed by six digits");
    }
}
```

## Error handling

- Use `Results.Problem()` for structured error responses
- Return appropriate HTTP status codes (400, 404, 500)
- Return useful error messages without leaking internals (no stack traces, no connection strings)
- Log the full exception server-side for debugging
- Use global exception handling middleware for unhandled exceptions

## Logging

- Use Serilog in Elastic Common Schema (ECS) format, configured via `appsettings.json`
- Log levels: `Fatal`, `Error`, `Warning`, `Information`, `Debug`, `Verbose`
- Never log PII: no names, addresses, emails, phone numbers, NI numbers, bank details, API keys, or tokens
- Include correlation IDs for request tracing
- Production: `Error` and above. Development: `Debug`

## Security

- Follow OWASP and Microsoft Secure Coding Guidelines
- Validate all input using FluentValidation
- Use parameterised queries — never concatenate user input
- Never commit secrets — use environment variables or a secrets manager
- Set security headers: `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`, HSTS
- Do not use `dynamic` types or reflection on user-supplied data

## Database access

- Use MongoDB.Driver for MongoDB (CDP platform default)
- For SQL, use Entity Framework Core or Dapper with parameterised queries
- Set connection timeouts and configure connection pooling
- Use repository or service patterns — do not query directly from endpoints

## Environment and configuration

- Load configuration from environment variables or `appsettings.json`
- Never hard-code secrets or connection strings
- Use `IOptions<T>` for typed configuration with environment-specific overrides
- Validate required configuration at startup — fail fast if values are missing

## Health checks

- Expose `/health` returning `200 OK` when healthy (CDP requirement)
- Include lightweight checks for database connectivity and critical dependencies

## Testing

- Use xUnit v3, FluentAssertions, and NSubstitute
- Use `Microsoft.AspNetCore.Mvc.Testing` for integration tests
- 90% minimum coverage — do not decrease existing baseline
- Test behaviour, not implementation; mock external dependencies
- Use `[ExcludeFromCodeCoverage]` only on bootstrapping code, never business logic

```csharp
public class ApplicationServiceTests
{
    private readonly IApplicationRepository _repository = Substitute.For<IApplicationRepository>();
    private readonly ApplicationService _sut;

    public ApplicationServiceTests() => _sut = new ApplicationService(_repository);

    [Fact]
    public async Task GetByIdAsync_WhenApplicationExists_ReturnsApplication()
    {
        var expected = new Application { Id = "123", Name = "Test" };
        _repository.GetByIdAsync("123").Returns(expected);
        var result = await _sut.GetByIdAsync("123");
        result.Should().BeEquivalentTo(expected);
    }
}
```

## Code style

- 4-space indentation, UTF-8, Allman brace style
- Private fields: `_camelCase`, constants: `PascalCase`
- Use predefined types (`int`, `string`) over framework types (`Int32`, `String`)
- Explicit accessibility modifiers on non-interface members

## Containers

- Use Defra base images: `defradigital/dotnetcore` (production), `defradigital/dotnetcore-development` (dev)
- Linux containers only, run as non-root, use multi-stage builds
- Tag images with semver; do not store secrets in images
- Use Docker Compose for local development

## Dependencies

- Use NuGet; only packages with OSI-approved licences compatible with OGL
- Run security scanning and resolve vulnerabilities before merging
- Configure Dependabot for automated updates

## References

- [Defra .NET standards](https://github.com/DEFRA/software-development-standards/blob/main/docs/standards/net_standards.md)
- [Defra C# coding standards](https://github.com/DEFRA/software-development-standards/blob/main/docs/standards/csharp_coding_standards.md)
- [CDP .NET backend template](https://github.com/DEFRA/cdp-dotnet-backend-template)
- [Microsoft C# conventions](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions)
- [OWASP Secure Coding Practices](https://owasp.org/www-project-secure-coding-practices-quick-reference-guide/)

Back to instructions index · Back to Getting Started