.NET Skills for Claude Code

repository·master·Indexed 21 days ago

https://github.com/aaronontheweb/dotnet-skills

A comprehensive plugin for professional .NET development featuring 30 skills and 5 specialized agents. It provides expertise in C# language patterns, Akka.NET, EF Core, .NET Aspire, and performance optimization. Compatible with Claude Code CLI, GitHub Copilot, and OpenCode, the library includes specialized agents for concurrency, performance analysis, benchmark design, Akka.NET, and DocFX.

Tokens
157.8K
Snippets
357
Records
427
Agent score
77%

What's inside dotnet-skills

  1. Configure Akka.NET with .NET Aspire

    master

    This skill provides a pattern for integrating Akka.NET into a .NET Aspire orchestrated environment. It covers setting up actor systems, clustering, persistence, and Akka.Management while leveraging Aspire for service orchestration and dependency management.

    Key principles include:

    • Using Microsoft.Extensions.Configuration for strongly-typed settings.
    • Using Akka.Hosting for ASP.NET Core dependency injection integration.
    • Using .NET Aspire to manage networking and environment configuration.
    • Implementing health checks for clustering and persistence.
    • Validating configuration at startup using IValidateOptions<T> and .ValidateOnStart().
  2. Overview of .NET Metric Instrument Types

    master

    In .NET, metric instruments are accessed via System.Diagnostics.Metrics.Meter. They are categorized into two main types based on how they are triggered:

    Synchronous Instruments

    These are called directly in your code when a specific event occurs. They are ideal for high-frequency events like request processing.

    • Counter: Monotonically increasing (only up). Use for request counts, errors, or bytes sent.
    • UpDownCounter: Can increase or decrease. Use for queue sizes or active connections.
    • Histogram: Captures distributions (e.g., latency percentiles). Use for request durations or response sizes.
    • Gauge (.NET 9+): Records an instantaneous snapshot. Use for non-cumulative measurements like temperature.

    Asynchronous/Observable Instruments

    These are called by the OpenTelemetry SDK at a defined collection interval via a callback. They are ideal for polling system state.

    • ObservableCounter: Reports a monotonically increasing value (e.g., total CPU time).
    • ObservableGauge: Reports an instantaneous value (e.g., current memory usage).
    • ObservableUpDownCounter: Reports a value that can go up or down (e.g., active tasks by priority).
    Is the value a cumulative total that only goes up?
      → YES: Can you increment it on every event?
               → YES: Counter (synchronous)
               → NO (polled periodically):  ObservableCounter (async)
      → NO:  Is it a distribution/percentile you need?
               → YES: Histogram
      → NO:  Can the value go both up AND down?
               → YES: Can you update it on every event?
                        → YES: UpDownCounter (synchronous)
                        → NO (polled periodically): ObservableUpDownCounter (async)
               → NO (instantaneous snapshot): Can you record it on every event?
                        → YES: Gauge (synchronous, .NET 9+)
                        → NO (polled periodically): ObservableGauge (async)
  3. What is a CRAP score and how is it calculated?

    master

    The CRAP (Change Risk Anti-Patterns) score identifies high-risk code by combining cyclomatic complexity with test coverage. It helps prioritize testing efforts on complex code that is also untested.

    Formula: CRAP Score = Complexity x (1 - Coverage)^2

    Risk Levels:

    • < 5 (Low): Well-tested, maintainable code.
    • 5-30 (Medium): Acceptable, but monitor complexity.
    • > 30 (High): High risk; requires immediate testing or refactoring.
  4. What is Aspire ServiceDefaults?

    master

    ServiceDefaults is a shared project used in .NET Aspire-based distributed applications to centralize common configuration across all services. It provides a unified way to manage:

    • OpenTelemetry: Standardized logging, tracing, and metrics.
    • Health Checks: Readiness and liveness endpoints.
    • Service Discovery: Automatic resolution of service names to addresses.
    • HTTP Resilience: Built-in retry and circuit breaker policies for HttpClient.

    By using a shared project, you ensure consistent observability and resilience patterns throughout your entire distributed system.

  5. Configure centralized build properties with Directory.Build.props

    master

    Place a Directory.Build.props file at your solution root to apply centralized build configurations to all projects in the directory tree. This is useful for managing metadata, C# language settings, versioning, and SourceLink configuration in one place.

    Key Patterns:

    • Dynamic Copyright: Use $([System.DateTime]::Now.Year) to automate copyright year updates.
    • Reusable Framework Properties: Define target framework versions once and reference them in individual .csproj files using $(PropertyName) syntax.
    • SourceLink: Configure PublishRepositoryUrl, EmbedUntrackedSources, IncludeSymbols, and SymbolPackageFormat to enable step-through debugging for NuGet packages.
    <Project>
      <PropertyGroup>
        <Copyright>Copyright © 2020-$([System.DateTime]::Now.Year) Your Company</Copyright>
        <LangVersion>latest</LangVersion>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
      </PropertyGroup>
    
      <PropertyGroup>
        <NetLibVersion>net8.0</NetLibVersion>
      </PropertyGroup>
    
      <PropertyGroup>
        <PublishRepositoryUrl>true</PublishRepositoryUrl>
        <EmbedUntrackedSources>true</EmbedUntrackedSources>
        <IncludeSymbols>true</IncludeSymbols>
        <SymbolPackageFormat>snupkg</SymbolPackageFormat>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
      </ItemGroup>
    </Project>
  6. Avoid creating custom test base classes in Akka.NET

    master

    Avoid creating abstract base classes (e.g., BaseAkkaTest : TestKit) to share setup logic. This creates tight coupling, hides dependencies, and makes per-test customization difficult.

    Instead, use method overrides. Each test class should override ConfigureServices() and ConfigureAkka() to define exactly the dependencies and configuration required for that specific test suite.

    // DO: Use Method Overrides
    // Each test class overrides ConfigureServices() and ConfigureAkka() with exactly what it needs.
  7. Use Span<T> and Memory<T> for zero-allocation code

    master

    For performance-critical code, use Span<T> and Memory<T> instead of byte[] or string to perform operations without unnecessary allocations.

    • Span<T>: Use for synchronous operations, stack-allocated buffers, and slicing data without allocation.
    • ReadOnlySpan<T>: Use for read-only views or method parameters where the data should not be modified.
    • Memory<T>: Use for asynchronous operations, as Span cannot cross await boundaries.
    • ReadOnlyMemory<T>: Use for read-only asynchronous operations.
    • ArrayPool<T>: Use for large temporary buffers (typically >1KB) to reduce Garbage Collector (GC) pressure by renting and returning arrays.
    • byte[]: Use only when you need to store data long-term or interface with APIs that specifically require arrays.
    // Span<T> for synchronous, zero-allocation operations
    public int ParseOrderId(ReadOnlySpan<char> input)
    {
        if (!input.StartsWith("ORD-"))
            throw new FormatException("Invalid order ID format");
    
        var numberPart = input.Slice(4);
        return int.Parse(numberPart);
    }
    
    // Memory<T> for async operations
    public async Task<int> ReadDataAsync(
        Memory<byte> buffer,
        CancellationToken cancellationToken)
    {
        return await _stream.ReadAsync(buffer, cancellationToken);
    }
    
    // ArrayPool for temporary large buffers
    public async Task ProcessLargeFileAsync(Stream stream, CancellationToken cancellationToken)
    {
        var buffer = ArrayPool<byte>.Shared.Rent(8192);
        try
        {
            int bytesRead;
            while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(), cancellationToken)) > 0)
            {
                ProcessChunk(buffer.AsSpan(0, bytesRead));
            }
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(buffer);
        }
    }
  8. Configure OpenTelemetry sampling

    master

    Sampling controls which traces are recorded and exported to reduce overhead.

    Built-in Samplers

    • AlwaysOnSampler: Records every span.
    • AlwaysOffSampler: Drops every span.
    • TraceIdRatioBasedSampler(ratio): Samples a fixed fraction (0.0–1.0).
    • ParentBasedSampler(root): Follows the parent's sampling decision (recommended for production). It uses a root sampler for new traces and follows the parent's decision for child spans.

    Configuration Methods

    Via Code (AddOpenTelemetry):

    builder.Services.AddOpenTelemetry()
        .WithTracing(tracing => tracing
            .AddSource("MyApp.MyComponent")
            .SetSampler(new ParentBasedSampler(
                new TraceIdRatioBasedSampler(0.1))) // 10% of root spans
            .AddOtlpExporter());

    Via Environment Variables:

    • OTEL_TRACES_SAMPLER: Set to always_on, always_off, traceidratio, or parentbased_traceidratio.
    • OTEL_TRACES_SAMPLER_ARG: The argument for the sampler (e.g., 0.1 for 10%).
    # Ratio-based sampling (10%)
    export OTEL_TRACES_SAMPLER=traceidratio
    export OTEL_TRACES_SAMPLER_ARG=0.1
    
    # Parent-based with ratio for roots
    export OTEL_TRACES_SAMPLER=parentbased_traceidratio
    export OTEL_TRACES_SAMPLER_ARG=0.1
  9. Understand the R3 concurrency contract

    master

    R3 does not serialize concurrent producers. It follows the Rx grammar: a source must not call OnNext concurrently or re-entrantly across threads. Operators like Where, Select, and Subject<T> are not internally locked for performance reasons. If multiple threads call OnNext on the same stream simultaneously, it will corrupt downstream state and may trigger exceptions (like ArgumentOutOfRangeException) that are surfaced via R3's global unhandled-exception handler rather than the caller.

    // DANGER: Concurrent producers without serialization
    var subject = new Subject<int>();
    subject.Where(x => x % 2 == 0).Subscribe(x => {
        // This list will likely be corrupted if subject.OnNext is called from multiple threads
        list.Add(x); 
    });
    
    Parallel.For(0, 20000, i => subject.OnNext(i));
  10. How R3's Observable and Observer model works

    master

    Unlike Rx.NET which uses interfaces, R3 uses abstract classes for its core types. This allows for centralized subscription tracking (e.g., via ObservableTracker).

    The R3 Grammar: (OnNext | OnErrorResume)* OnCompleted(Result)?

    Key differences from Rx:

    • Errors are decoupled from termination: OnErrorResume allows you to handle an error without killing the subscription. Only OnCompleted ends the stream.
    • Termination carries a Result: OnCompleted provides a Result object which indicates if the stream ended via Result.Success or Result.Failure(exception).
    public abstract class Observable<T>
    {
        public IDisposable Subscribe(Observer<T> observer);     // tracked centrally
        protected abstract IDisposable SubscribeCore(Observer<T> observer);
    }
    
    public abstract class Observer<T> : IDisposable               // the observer IS the subscription
    {
        public void OnNext(T value);
        public void OnErrorResume(Exception error);               // error WITHOUT unsubscribing
        public void OnCompleted(Result result);                   // success OR failure terminates
    }
  11. Use records for immutable data

    master

    Use record types for DTOs, messages, events, and domain entities to ensure immutability by default.

    • Use record class (the default) for reference types like entities, aggregates, and DTOs with multiple properties.
    • Use IReadOnlyList<T> for collections within records to prevent mutation of the underlying collection.
    • Use init-only properties for controlled initialization.
    // Simple immutable DTO
    public record CustomerDto(string Id, string Name, string Email);
    
    // Record with validation in constructor
    public record EmailAddress
    {
        public string Value { get; init; }
    
        public EmailAddress(string value)
        {
            if (string.IsNullOrWhiteSpace(value) || !value.Contains('@'))
                throw new ArgumentException("Invalid email address", nameof(value));
    
            Value = value;
        }
    }
    
    // Records with collections - use IReadOnlyList
    public record ShoppingCart(
        string CartId,
        string CustomerId,
        IReadOnlyList<CartItem> Items
    ) 
    {
        public decimal Total => Items.Sum(item => item.Price * item.Quantity);
    }