All skills
wshaddix avatar

/csharp-coding-standards

@e4caaf0

Write modern, high-performance C# code using records, pattern matching, value objects, async/await, Span<T>/Memory<T>, and best-practice API design patterns. Emphasizes functional-style programming with C# 12+ features. Use when writing new C# code or refactoring existing code, designing public APIs for libraries or services, optimizing performance-critical code paths, or building async/await-heavy applications.

  • 6 files
  • 53.4 KB
  • Updated 7 months ago
  • GitHub

Use this Skill: https://skilld.dev/gh/wshaddix/dotnet-skills/csharp-coding-standards

This session only. Nothing lands on disk.

referenceanti-patterns.md

≈2.1k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Anti-Patterns

Avoid Reflection-Based Metaprogramming

Prefer statically-typed, explicit code over reflection-based "magic" libraries.

Reflection-based libraries like AutoMapper trade compile-time safety for convenience. When mappings break, you find out at runtime (or worse, in production) instead of at compile time.

Banned Libraries

Library Problem
AutoMapper Reflection magic, hidden mappings, runtime failures, hard to debug
Mapster Same issues as AutoMapper
ExpressMapper Same issues

Why Reflection Mapping Fails

// With AutoMapper - compiles fine, fails at runtime
public record UserDto(string Id, string Name, string Email);
public record UserEntity(Guid Id, string FullName, string EmailAddress);

// This mapping silently produces garbage:
// - Id: string vs Guid mismatch
// - Name vs FullName: no match, null/default
// - Email vs EmailAddress: no match, null/default
var dto = _mapper.Map<UserDto>(entity);  // Compiles! Breaks at runtime.

Use Explicit Mapping Methods Instead

// Extension method - compile-time checked, easy to find, easy to debug
public static class UserMappings
{
    public static UserDto ToDto(this UserEntity entity) => new(
        Id: entity.Id.ToString(),
        Name: entity.FullName,
        Email: entity.EmailAddress);

    public static UserEntity ToEntity(this CreateUserRequest request) => new(
        Id: Guid.NewGuid(),
        FullName: request.Name,
        EmailAddress: request.Email);
}

// Usage - explicit and traceable
var dto = entity.ToDto();
var entity = request.ToEntity();

Benefits of Explicit Mappings

Aspect AutoMapper Explicit Methods
Compile-time safety No - runtime errors Yes - compiler catches mismatches
Discoverability Hidden in profiles "Go to Definition" works
Debugging Black box Step through code
Refactoring Rename breaks silently IDE renames correctly
Performance Reflection overhead Direct property access
Testing Need integration tests Simple unit tests

Complex Mappings

For complex transformations, explicit code is even more valuable:

public static OrderSummaryDto ToSummary(this Order order) => new(
    OrderId: order.Id.Value.ToString(),
    CustomerName: order.Customer.FullName,
    ItemCount: order.Items.Count,
    Total: order.Items.Sum(i => i.Quantity * i.UnitPrice),
    Status: order.Status switch
    {
        OrderStatus.Pending => "Awaiting Payment",
        OrderStatus.Paid => "Processing",
        OrderStatus.Shipped => "On the Way",
        OrderStatus.Delivered => "Completed",
        _ => "Unknown"
    },
    FormattedDate: order.CreatedAt.ToString("MMMM d, yyyy"));

This is:

  • Readable: Anyone can understand the transformation
  • Debuggable: Set a breakpoint, inspect values
  • Testable: Pass an Order, assert on the result
  • Refactorable: Change a property name, compiler tells you everywhere it's used

When Reflection is Acceptable

Reflection has legitimate uses, but mapping DTOs isn't one of them:

Use Case Acceptable?
Serialization (System.Text.Json, Newtonsoft) Yes - well-tested, source generators available
Dependency injection container Yes - framework infrastructure
ORM entity mapping (EF Core) Yes - necessary for database abstraction
Test fixtures and builders Sometimes - for convenience in tests only
DTO/domain object mapping No - use explicit methods

UnsafeAccessorAttribute (.NET 8+)

When you genuinely need to access private or internal members (serializers, test helpers, framework code), use UnsafeAccessorAttribute instead of traditional reflection. It provides zero-overhead, AOT-compatible member access.

// AVOID: Traditional reflection - slow, allocates, breaks AOT
var field = typeof(Order).GetField("_status", BindingFlags.NonPublic | BindingFlags.Instance);
var status = (OrderStatus)field!.GetValue(order)!;

// PREFER: UnsafeAccessor - zero overhead, AOT-compatible
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_status")]
static extern ref OrderStatus GetStatusField(Order order);

var status = GetStatusField(order);  // Direct access, no reflection

Supported accessor kinds:

// Private field access
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_items")]
static extern ref List<OrderItem> GetItemsField(Order order);

// Private method access
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "Recalculate")]
static extern void CallRecalculate(Order order);

// Private static field
[UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = "_instanceCount")]
static extern ref int GetInstanceCount(Order order);

// Private constructor
[UnsafeAccessor(UnsafeAccessorKind.Constructor)]
static extern Order CreateOrder(OrderId id, CustomerId customerId);

Why UnsafeAccessor over reflection:

Aspect Reflection UnsafeAccessor
Performance Slow (100-1000x) Zero overhead
AOT compatible No Yes
Allocations Yes (boxing, arrays) None
Compile-time checked No Partially (signature)

Use cases:

  • Serializers accessing private backing fields
  • Test helpers verifying internal state
  • Framework code that needs to bypass visibility

Resources:


Anti-Patterns to Avoid

❌ DON'T: Use mutable DTOs

// BAD: Mutable DTO
public class CustomerDto
{
    public string Id { get; set; }
    public string Name { get; set; }
}

// GOOD: Immutable record
public record CustomerDto(string Id, string Name);

❌ DON'T: Use classes for value objects

// BAD: Value object as class
public class OrderId
{
    public string Value { get; }
    public OrderId(string value) => Value = value;
}

// GOOD: Value object as readonly record struct
public readonly record struct OrderId(string Value);

❌ DON'T: Create deep inheritance hierarchies

// BAD: Deep inheritance
public abstract class Entity { }
public abstract class AggregateRoot : Entity { }
public abstract class Order : AggregateRoot { }
public class CustomerOrder : Order { }

// GOOD: Flat structure with composition
public interface IEntity
{
    Guid Id { get; }
}

public record Order(OrderId Id, CustomerId CustomerId, Money Total) : IEntity
{
    Guid IEntity.Id => Id.Value;
}

❌ DON'T: Return List<T> when you mean IReadOnlyList<T>

// BAD: Exposes internal list for modification
public List<Order> GetOrders() => _orders;

// GOOD: Returns read-only view
public IReadOnlyList<Order> GetOrders() => _orders;

❌ DON'T: Use byte[] when ReadOnlySpan<byte> works

// BAD: Allocates array on every call
public byte[] GetHeader()
{
    var header = new byte[64];
    // Fill header
    return header;
}

// GOOD: Zero allocation with Span
public void GetHeader(Span<byte> destination)
{
    if (destination.Length < 64)
        throw new ArgumentException("Buffer too small");

    // Fill header directly into caller's buffer
}

❌ DON'T: Forget CancellationToken in async methods

// BAD: No cancellation support
public async Task<Order> GetOrderAsync(OrderId id)
{
    return await _repository.GetAsync(id);
}

// GOOD: Cancellation support
public async Task<Order> GetOrderAsync(
    OrderId id,
    CancellationToken cancellationToken = default)
{
    return await _repository.GetAsync(id, cancellationToken);
}

❌ DON'T: Block on async code

// BAD: Deadlock risk!
public Order GetOrder(OrderId id)
{
    return GetOrderAsync(id).Result;
}

// BAD: Also deadlock risk!
public Order GetOrder(OrderId id)
{
    return GetOrderAsync(id).GetAwaiter().GetResult();
}

// GOOD: Async all the way
public async Task<Order> GetOrderAsync(
    OrderId id,
    CancellationToken cancellationToken)
{
    return await _repository.GetAsync(id, cancellationToken);
}

Source: SKILL.md on GitHub

No third-party reports yet.

Signed by skilld at e4caaf0. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 months ago.

Dormantupdated 7 months ago

README badge

README badge for wshaddix/dotnet-skills/csharp-coding-standards