csharp-guide

Applies to: C# 12+, .NET 8+, ASP.NET Core, Console Apps, Libraries

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "csharp-guide" with this command: npx skills add ar4mirez/samuel/ar4mirez-samuel-csharp-guide

C# Guide

Applies to: C# 12+, .NET 8+, ASP.NET Core, Console Apps, Libraries

Core Principles

  • Type Safety: Enable nullable reference types project-wide; treat warnings as errors

  • Immutability First: Prefer records, readonly , and init properties for data types

  • Async All The Way: Use async /await end-to-end; never block on async code

  • Dependency Injection: Constructor injection via IServiceCollection ; no service locator

  • Fail Fast: Validate inputs at boundaries; use guard clauses and ArgumentException

Guardrails

Version & Dependencies

  • Target .NET 8+ with C# 12+ language features

  • Use <Nullable>enable</Nullable> and <ImplicitUsings>enable</ImplicitUsings> in .csproj

  • Pin package versions explicitly in .csproj (avoid floating * versions)

  • Run dotnet restore before committing after dependency changes

  • Audit new packages with dotnet list package --vulnerable before adding

Code Style

  • Follow .NET naming conventions

  • Public members: PascalCase | Private fields: _camelCase with underscore prefix

  • Interfaces: IServiceName | Async methods: suffix with Async

  • Use var when the type is obvious from the right side; explicit types otherwise

  • One class per file; filename matches class name

  • Use file-scoped namespaces (namespace MyApp.Services; )

  • Configure .editorconfig for consistent formatting across the team

  • Run dotnet format before every commit

Nullable Reference Types

  • Enable globally: <Nullable>enable</Nullable> in every .csproj

  • Never suppress warnings with #pragma warning disable unless documented

  • Use [NotNullWhen] , [MaybeNullWhen] , [NotNull] attributes for complex nullability

  • Use the null-forgiving operator ! sparingly and only with a justifying comment

  • Prefer is not null over != null for null checks

  • Use ?? and ??= for default values; ?. for conditional access

// Good: explicit nullability contract public User? FindByEmail(string email) { ArgumentException.ThrowIfNullOrWhiteSpace(email); return _users.FirstOrDefault(u => u.Email == email); }

// Good: guard clause with null-coalescing public void Process(Order order) { var customer = order.Customer ?? throw new InvalidOperationException("Order must have a customer."); // ... }

Async/Await

  • Use async /await end-to-end; never call .Result or .Wait() (deadlock risk)

  • Suffix all async methods with Async : GetUserAsync , SaveOrderAsync

  • Use ValueTask<T> for hot paths that frequently complete synchronously

  • Use ConfigureAwait(false) in library code (not in ASP.NET Core controllers)

  • Use CancellationToken in all async method signatures that perform I/O

  • Use IAsyncEnumerable<T> for streaming large result sets

  • Set timeouts on all external calls with CancellationTokenSource

public async Task<User> GetUserAsync( int id, CancellationToken cancellationToken = default) { return await _dbContext.Users .AsNoTracking() .FirstOrDefaultAsync(u => u.Id == id, cancellationToken) ?? throw new NotFoundException(nameof(User), id); }

LINQ

  • Prefer method syntax over query syntax for consistency

  • Never put side effects inside LINQ queries (no mutations, no I/O)

  • Use AsNoTracking() for read-only EF Core queries

  • Materialize queries with ToListAsync() / ToArrayAsync() before returning

  • Avoid Count() when Any() suffices

  • Use Select to project only needed fields (avoid loading full entities)

// Good: projection, no-tracking, materialized var activeEmails = await _dbContext.Users .AsNoTracking() .Where(u => u.IsActive) .Select(u => u.Email) .ToListAsync(cancellationToken);

// Bad: side effect in LINQ var results = items.Select(i => { i.Processed = true; return i; }); // Don't do this

Project Structure

MySolution/ ├── MySolution.sln ├── src/ │ ├── MySolution.Api/ # ASP.NET Core host / entry point │ │ ├── Controllers/ │ │ ├── Middleware/ │ │ ├── Program.cs │ │ └── MySolution.Api.csproj │ ├── MySolution.Application/ # Use cases, commands, queries (CQRS) │ │ ├── Commands/ │ │ ├── Queries/ │ │ ├── Interfaces/ │ │ └── MySolution.Application.csproj │ ├── MySolution.Domain/ # Entities, value objects, domain events │ │ ├── Entities/ │ │ ├── ValueObjects/ │ │ ├── Exceptions/ │ │ └── MySolution.Domain.csproj │ └── MySolution.Infrastructure/ # EF Core, external services, file I/O │ ├── Persistence/ │ ├── Services/ │ └── MySolution.Infrastructure.csproj ├── tests/ │ ├── MySolution.UnitTests/ │ │ └── MySolution.UnitTests.csproj │ ├── MySolution.IntegrationTests/ │ │ └── MySolution.IntegrationTests.csproj │ └── MySolution.ArchTests/ # Architecture rule tests (optional) │ └── MySolution.ArchTests.csproj ├── .editorconfig ├── Directory.Build.props # Shared build properties └── README.md

  • Domain project has zero external dependencies (pure C#)

  • Application depends only on Domain

  • Infrastructure depends on Application and Domain

  • Api depends on all projects (composition root)

  • Test projects mirror src/ structure

Key Patterns

Nullable Reference Types & Guard Clauses

public sealed class OrderService { private readonly IOrderRepository _repository;

public OrderService(IOrderRepository repository)
{
    _repository = repository ?? throw new ArgumentNullException(nameof(repository));
}

public async Task&#x3C;OrderSummary> GetSummaryAsync(
    int orderId, CancellationToken ct = default)
{
    ArgumentOutOfRangeException.ThrowIfNegativeOrZero(orderId);

    var order = await _repository.GetByIdAsync(orderId, ct)
        ?? throw new NotFoundException(nameof(Order), orderId);

    return new OrderSummary(order.Id, order.Total, order.Status);
}

}

Pattern Matching

// Switch expression with property patterns public decimal CalculateDiscount(Customer customer) => customer switch { { MembershipLevel: "Gold", YearsActive: > 5 } => 0.20m, { MembershipLevel: "Gold" } => 0.15m, { MembershipLevel: "Silver" } => 0.10m, { TotalOrders: > 100 } => 0.05m, _ => 0m, };

// Relational and logical patterns public string ClassifyTemperature(double temp) => temp switch { < 0 => "Freezing", >= 0 and < 15 => "Cold", >= 15 and < 25 => "Moderate", >= 25 and < 35 => "Warm", >= 35 => "Hot", };

// Type pattern in is-expression public static string Describe(object value) => value switch { int n when n < 0 => $"Negative integer: {n}", int n => $"Positive integer: {n}", string { Length: 0 } => "Empty string", string s => $"String of length {s.Length}", null => "null", _ => $"Unknown: {value.GetType().Name}", };

Records & Immutable Data

// Record for DTOs and value objects (value equality, immutable) public sealed record OrderSummary(int Id, decimal Total, OrderStatus Status);

// Record with validation public sealed record EmailAddress { public string Value { get; }

public EmailAddress(string value)
{
    ArgumentException.ThrowIfNullOrWhiteSpace(value);
    if (!value.Contains('@'))
        throw new ArgumentException("Invalid email format.", nameof(value));
    Value = value;
}

}

// Record struct for high-performance value types (no heap allocation) public readonly record struct Coordinate(double Latitude, double Longitude);

// Nondestructive mutation with with var updated = original with { Status = OrderStatus.Shipped };

Async Streams

// Producing an async stream public async IAsyncEnumerable<LogEntry> StreamLogsAsync( DateTime since, [EnumeratorCancellation] CancellationToken ct = default) { await foreach (var batch in _logSource.ReadBatchesAsync(since, ct)) { foreach (var entry in batch.Entries) { ct.ThrowIfCancellationRequested(); yield return entry; } } }

// Consuming an async stream await foreach (var log in StreamLogsAsync(DateTime.UtcNow.AddHours(-1), ct)) { Console.WriteLine($"[{log.Timestamp}] {log.Message}"); }

Dependency Injection

// Registration in Program.cs (or a ServiceCollectionExtensions class) public static IServiceCollection AddApplicationServices( this IServiceCollection services) { services.AddScoped<IOrderRepository, OrderRepository>(); services.AddScoped<IOrderService, OrderService>(); services.AddSingleton<IClock, SystemClock>(); services.AddHttpClient<IPaymentGateway, StripePaymentGateway>(client => { client.BaseAddress = new Uri("https://api.stripe.com/"); client.Timeout = TimeSpan.FromSeconds(10); });

return services;

}

// Constructor injection (no service locator, no static access) public sealed class OrderService : IOrderService { private readonly IOrderRepository _repository; private readonly IClock _clock;

public OrderService(IOrderRepository repository, IClock clock)
{
    _repository = repository;
    _clock = clock;
}

}

Testing

Standards

  • Framework: xUnit (preferred), with [Fact] and [Theory]

  • Mocking: NSubstitute or Moq (pick one per project, stay consistent)

  • Assertions: FluentAssertions for readable assertion syntax

  • Test naming: MethodName_Scenario_ExpectedResult

  • One assertion concept per test (multiple Should calls for same concept OK)

  • Use [Theory] with [InlineData] for parameterized tests

  • Coverage target: >80% for business logic, >60% overall

Unit Test Example

public sealed class OrderServiceTests { private readonly IOrderRepository _repository = Substitute.For<IOrderRepository>(); private readonly IClock _clock = Substitute.For<IClock>(); private readonly OrderService _sut;

public OrderServiceTests()
{
    _sut = new OrderService(_repository, _clock);
}

[Fact]
public async Task GetSummaryAsync_ExistingOrder_ReturnsSummary()
{
    // Arrange
    var order = new Order { Id = 1, Total = 99.99m, Status = OrderStatus.Pending };
    _repository.GetByIdAsync(1, Arg.Any&#x3C;CancellationToken>()).Returns(order);

    // Act
    var result = await _sut.GetSummaryAsync(1);

    // Assert
    result.Should().NotBeNull();
    result.Id.Should().Be(1);
    result.Total.Should().Be(99.99m);
    result.Status.Should().Be(OrderStatus.Pending);
}

[Fact]
public async Task GetSummaryAsync_MissingOrder_ThrowsNotFoundException()
{
    // Arrange
    _repository.GetByIdAsync(99, Arg.Any&#x3C;CancellationToken>())
        .Returns((Order?)null);

    // Act
    var act = () => _sut.GetSummaryAsync(99);

    // Assert
    await act.Should().ThrowAsync&#x3C;NotFoundException>()
        .WithMessage("*Order*99*");
}

[Theory]
[InlineData(0)]
[InlineData(-1)]
[InlineData(-100)]
public async Task GetSummaryAsync_InvalidId_ThrowsArgumentException(int invalidId)
{
    var act = () => _sut.GetSummaryAsync(invalidId);

    await act.Should().ThrowAsync&#x3C;ArgumentOutOfRangeException>();
}

}

Integration Test Example

public sealed class OrdersApiTests : IClassFixture<WebApplicationFactory<Program>> { private readonly HttpClient _client;

public OrdersApiTests(WebApplicationFactory&#x3C;Program> factory)
{
    _client = factory.WithWebHostBuilder(builder =>
    {
        builder.ConfigureServices(services =>
        {
            // Replace real DB with in-memory for tests
            services.RemoveAll&#x3C;DbContextOptions&#x3C;AppDbContext>>();
            services.AddDbContext&#x3C;AppDbContext>(opts =>
                opts.UseInMemoryDatabase("TestDb"));
        });
    }).CreateClient();
}

[Fact]
public async Task GetOrder_ReturnsOk_WhenOrderExists()
{
    var response = await _client.GetAsync("/api/orders/1");

    response.StatusCode.Should().Be(HttpStatusCode.OK);
    var body = await response.Content.ReadFromJsonAsync&#x3C;OrderSummary>();
    body.Should().NotBeNull();
    body!.Id.Should().Be(1);
}

}

Tooling

Essential Commands

dotnet new sln # Create solution dotnet new webapi -n MyApp.Api # New Web API project dotnet sln add src/MyApp.Api # Add project to solution dotnet restore # Restore packages dotnet build --no-restore # Build dotnet test --no-build --verbosity normal # Run tests dotnet test --collect:"XPlat Code Coverage" # With coverage dotnet format # Format code dotnet publish -c Release -o ./publish # Publish for deployment

Analyzers & EditorConfig

<!-- Directory.Build.props (shared across all projects) --> <Project> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> <AnalysisLevel>latest-recommended</AnalysisLevel> </PropertyGroup>

<ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0" PrivateAssets="all" /> <PackageReference Include="SonarAnalyzer.CSharp" Version="9.32.0" PrivateAssets="all" /> </ItemGroup> </Project>

.editorconfig (key settings)

[*.cs] indent_style = space indent_size = 4 dotnet_sort_system_directives_first = true csharp_style_namespace_declarations = file_scoped:warning csharp_style_var_for_built_in_types = false:suggestion csharp_style_var_when_type_is_apparent = true:suggestion csharp_style_prefer_switch_expression = true:suggestion csharp_style_prefer_pattern_matching = true:suggestion csharp_prefer_simple_using_statement = true:suggestion dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning

References

For detailed patterns and examples, see:

  • references/patterns.md -- Async patterns, DI registration, LINQ examples

External References

  • C# Language Reference

  • .NET Naming Conventions

  • Nullable Reference Types

  • Async/Await Best Practices

  • xUnit Documentation

  • FluentAssertions

  • NSubstitute

  • .NET Architecture Guides

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

frontend-design

No summary provided by upstream source.

Repository SourceNeeds Review
General

blazor

No summary provided by upstream source.

Repository SourceNeeds Review
General

rust-guide

No summary provided by upstream source.

Repository SourceNeeds Review
General

go-guide

No summary provided by upstream source.

Repository SourceNeeds Review