From 2fa39e97bd8bb914a34dcf58573f3eb7c15aaf0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jani=20Simil=C3=A4?= <15038593+janitorr@users.noreply.github.com> Date: Wed, 25 Mar 2026 00:09:51 +0200 Subject: [PATCH] Feature/add dotnet support (#275) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📝 docs: add C# and ASP.NET Core context files for agent guidance * 📝 docs: add C# / .NET to supported languages across docs * docs: bump C# project structure to v1.4.0 with project initialization section --- .../standards/csharp-project-structure.md | 669 +++++++++++++ .opencode/context/core/standards/csharp.md | 903 ++++++++++++++++++ .../context/core/standards/navigation.md | 15 + README.md | 6 +- docs/agents/opencoder.md | 11 +- 5 files changed, 1599 insertions(+), 5 deletions(-) create mode 100644 .opencode/context/core/standards/csharp-project-structure.md create mode 100644 .opencode/context/core/standards/csharp.md diff --git a/.opencode/context/core/standards/csharp-project-structure.md b/.opencode/context/core/standards/csharp-project-structure.md new file mode 100644 index 0000000..d9512a3 --- /dev/null +++ b/.opencode/context/core/standards/csharp-project-structure.md @@ -0,0 +1,669 @@ +--- +id: csharp-project-structure +name: C# Project Structure +description: "ASP.NET Core project structure with Minimal APIs, CQRS, EF Core, and PostgreSQL patterns" +category: core +type: standard +version: 1.4.0 +author: community +--- + + + +# C# Project Structure Standards + +**Purpose**: Standard project structure for ASP.NET Core APIs using Minimal APIs, CQRS (MediatR), Vertical Slice Architecture, and EF Core with PostgreSQL +**Scope**: Project layout, wiring patterns, and conventions — not language-level rules (see `csharp.md`) +**Last Updated**: 2026-03-15 + +--- + +## Design Principles + +- **`Features/`** contains only business logic — Commands, Queries, Validators, Handlers. No HTTP or framework code. +- **`Features/Common/`** contains shared feature-level concerns — pipeline behaviors, shared exceptions. +- **`Infrastructure/`** contains all technical wiring — endpoint registration, DB context, external services. +- **Command/Query records are the API contract** — they are sent directly from endpoints. No separate DTO mapping layer. + +--- + +## Table of Contents + +1. [Project Initialization](#1-project-initialization) +2. [Project Layout](#2-project-layout) +3. [Program.cs](#3-programcs) +4. [Features — Vertical Slices](#4-features--vertical-slices) +5. [Infrastructure](#5-infrastructure) +6. [EF Core & PostgreSQL](#6-ef-core--postgresql) +7. [Migrations](#7-migrations) +8. [NuGet Packages](#8-nuget-packages) + +--- + +## 1. Project Initialization + +**Always run these commands FIRST when creating a new ASP.NET Core project:** + +```bash +# Create .gitignore (C# patterns) +dotnet new gitignore + +# Create .gitattributes (normalize line endings) +dotnet new gitattributes + +# Create the project +dotnet new web -n MyApi +cd MyApi +``` + +**Why this order?** +- `.gitignore` must exist before any build artifacts are created (prevents committing `bin/`, `obj/`, etc.) +- `.gitattributes` ensures consistent line endings across team members +- Projects created after these are in place benefit from proper version control setup + +--- + +## 2. Project Layout + +``` +MyApi/ +├── Program.cs # DI wiring + endpoint mapping only +├── MyApi.csproj +│ +├── Api/ # Endpoint entry points — discover processes here +│ ├── OrderEndpoints.cs # IEndpointRouteBuilder extension — all /orders routes +│ ├── UserEndpoints.cs +│ └── ProductEndpoints.cs +│ +├── Features/ # Pure business logic — no HTTP/framework code +│ ├── Orders/ +│ │ ├── CreateOrder.cs # Command + Validator + Handler (co-located) +│ │ ├── GetOrder.cs # Query + Handler +│ │ ├── GetAllOrders.cs # Query + Handler +│ │ └── CancelOrder.cs # Command + Handler + Domain Event +│ │ +│ ├── Users/ +│ │ ├── CreateUser.cs +│ │ └── GetUser.cs +│ │ +│ ├── Products/ +│ │ └── CreateProduct.cs +│ │ +│ └── Common/ # Shared feature-level concerns +│ ├── Behaviors/ +│ │ ├── LoggingBehavior.cs +│ │ └── ValidationBehavior.cs +│ └── Exceptions/ +│ ├── NotFoundException.cs +│ └── ValidationException.cs +│ +├── Infrastructure/ # All framework/technical wiring (non-endpoint) +│ ├── Persistence/ +│ │ ├── AppDbContext.cs +│ │ ├── Configurations/ # IEntityTypeConfiguration classes +│ │ │ ├── OrderConfiguration.cs +│ │ │ └── UserConfiguration.cs +│ │ └── Migrations/ # EF Core migrations (auto-generated) +│ ├── Services/ # External HTTP clients, email, storage, etc. +│ └── Extensions/ +│ └── InfrastructureExtensions.cs # AddInfrastructure() registration +│ +├── Domain/ # Optional: rich domain model (for DDD projects) +│ ├── Entities/ +│ └── Events/ +│ +├── appsettings.json +├── appsettings.Development.json +└── GlobalUsings.cs # global using directives +``` + +--- + +## 3. Program.cs + +`Program.cs` contains **only** DI registration and endpoint mapping. No business logic. + +```csharp +// Program.cs +using MyApi.Api; +using MyApi.Infrastructure.Extensions; + +var builder = WebApplication.CreateBuilder(args); + +// ── Infrastructure (DB, external services) ─────────────────────────────── +builder.Services.AddInfrastructure(builder.Configuration); + +// ── MediatR (CQRS) ─────────────────────────────────────────────────────── +builder.Services.AddMediatR(cfg => +{ + cfg.RegisterServicesFromAssembly(typeof(Program).Assembly); + cfg.AddOpenBehavior(typeof(LoggingBehavior<,>)); + cfg.AddOpenBehavior(typeof(ValidationBehavior<,>)); +}); + +// ── Validation ──────────────────────────────────────────────────────────── +builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly); + +// ── OpenAPI ─────────────────────────────────────────────────────────────── +builder.Services.AddOpenApi(); // .NET 9 native; or Swashbuckle for earlier versions + +// ── Auth ────────────────────────────────────────────────────────────────── +builder.Services.AddAuthentication().AddJwtBearer(); +builder.Services.AddAuthorization(); + +// ── Build ───────────────────────────────────────────────────────────────── +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) + app.MapOpenApi(); + +app.UseAuthentication(); +app.UseAuthorization(); + +// ── Endpoint registration ───────────────────────────────────────────────── +app.MapOrderEndpoints(); +app.MapUserEndpoints(); +app.MapProductEndpoints(); + +app.Run(); + +public partial class Program { } // allows WebApplicationFactory in integration tests +``` + +--- + +## 4. Features — Vertical Slices + +Each use case lives in its own file: **Command or Query record + Validator + Handler — nothing else**. +The Command/Query record is the API contract — it is bound directly from the HTTP request body/route. +No separate DTO types, no mapping layer. + +### 3.1 Command Slice (write operation) + +```csharp +// Features/Orders/CreateOrder.cs +namespace MyApi.Features.Orders; + +// ── Command = API request contract ─────────────────────────────────────── +// Bound directly from HTTP request body. No separate DTO needed. +public record CreateOrderCommand(Guid UserId, List Items) : IRequest; + +// Return type is also a record — represents the response shape +public record CreatedOrderResult(Guid Id, Guid UserId, DateTime CreatedAt); + +// ── Validator ───────────────────────────────────────────────────────────── +public class CreateOrderValidator : AbstractValidator +{ + public CreateOrderValidator() + { + RuleFor(x => x.UserId).NotEmpty(); + RuleFor(x => x.Items).NotEmpty().WithMessage("Order must contain at least one item."); + } +} + +// ── Handler ─────────────────────────────────────────────────────────────── +public class CreateOrderHandler(AppDbContext db) : IRequestHandler +{ + public async Task Handle(CreateOrderCommand cmd, CancellationToken ct) + { + var order = new Order + { + Id = Guid.NewGuid(), + UserId = cmd.UserId, + Items = cmd.Items, + CreatedAt = DateTime.UtcNow, + }; + + db.Orders.Add(order); + await db.SaveChangesAsync(ct); + + return new CreatedOrderResult(order.Id, order.UserId, order.CreatedAt); + } +} +``` + +### 3.2 Query Slice (read operation) + +```csharp +// Features/Orders/GetOrder.cs +namespace MyApi.Features.Orders; + +// ── Query = API request contract ────────────────────────────────────────── +// Route parameter bound directly. Return type is the response shape. +public record GetOrderQuery(Guid Id) : IRequest; + +public record OrderResult(Guid Id, Guid UserId, OrderStatus Status, DateTime CreatedAt); + +// ── Handler ─────────────────────────────────────────────────────────────── +public class GetOrderHandler(AppDbContext db) : IRequestHandler +{ + public async Task Handle(GetOrderQuery query, CancellationToken ct) + => await db.Orders + .AsNoTracking() + .Where(o => o.Id == query.Id) + .Select(o => new OrderResult(o.Id, o.UserId, o.Status, o.CreatedAt)) + .FirstOrDefaultAsync(ct); +} +``` + +### 3.3 Domain Events (INotification) + +```csharp +// Features/Orders/CancelOrder.cs +namespace MyApi.Features.Orders; + +public record CancelOrderCommand(Guid Id) : IRequest; + +// ── Domain event ────────────────────────────────────────────────────────── +public record OrderCancelledEvent(Guid OrderId) : INotification; + +// ── Handler ─────────────────────────────────────────────────────────────── +public class CancelOrderHandler(AppDbContext db, IPublisher publisher) + : IRequestHandler +{ + public async Task Handle(CancelOrderCommand cmd, CancellationToken ct) + { + var order = await db.Orders.FindAsync([cmd.Id], ct); + if (order is null) return false; + + order.Status = OrderStatus.Cancelled; + await db.SaveChangesAsync(ct); + + await publisher.Publish(new OrderCancelledEvent(order.Id), ct); + return true; + } +} + +// ── Side-effect handlers (each independent, all run on Publish) ────────── +public class SendCancellationEmailHandler(IEmailService email) + : INotificationHandler +{ + public async Task Handle(OrderCancelledEvent e, CancellationToken ct) + => await email.SendCancellationAsync(e.OrderId, ct); +} +``` + +### 3.4 Features/Common — Pipeline Behaviors + +```csharp +// Features/Common/Behaviors/LoggingBehavior.cs +namespace MyApi.Features.Common.Behaviors; + +public class LoggingBehavior(ILogger> logger) + : IPipelineBehavior + where TRequest : notnull +{ + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken ct) + { + var name = typeof(TRequest).Name; + logger.LogInformation("Handling {Request}", name); + var sw = Stopwatch.StartNew(); + try + { + var response = await next(); + logger.LogInformation("Handled {Request} in {ElapsedMs}ms", name, sw.ElapsedMilliseconds); + return response; + } + catch (Exception ex) + { + logger.LogError(ex, "Error handling {Request} after {ElapsedMs}ms", name, sw.ElapsedMilliseconds); + throw; + } + } +} +``` + +```csharp +// Features/Common/Behaviors/ValidationBehavior.cs +namespace MyApi.Features.Common.Behaviors; + +public class ValidationBehavior(IEnumerable> validators) + : IPipelineBehavior + where TRequest : notnull +{ + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken ct) + { + if (!validators.Any()) return await next(); + + var context = new ValidationContext(request); + var results = await Task.WhenAll(validators.Select(v => v.ValidateAsync(context, ct))); + var failures = results.SelectMany(r => r.Errors).Where(f => f is not null).ToList(); + + if (failures.Count > 0) + throw new ValidationException(failures); + + return await next(); + } +} +``` + +--- + +## 5. Infrastructure & API Endpoints + +### 4.1 API Endpoints (Entry Points) + +Endpoint files live in the `Api/` directory at the project root — they are the entry points for discovering business processes. These files handle HTTP concerns only: routing, parameter binding, response shaping, and dispatching to MediatR. No business logic here. + +```csharp +// Api/OrderEndpoints.cs +namespace MyApi.Api; + +public static class OrderEndpoints +{ + public static void MapOrderEndpoints(this IEndpointRouteBuilder app) + { + var group = app.MapGroup("/orders") + .WithTags("Orders") + .WithOpenApi() + .RequireAuthorization(); + + group.MapGet("/", GetAllOrders).WithName("GetAllOrders"); + group.MapGet("/{id}", GetOrder) .WithName("GetOrder"); + group.MapPost("/", CreateOrder) .WithName("CreateOrder"); + group.MapDelete("/{id}", CancelOrder) .WithName("CancelOrder"); + } + + // Command/Query records are bound directly from HTTP — no mapping needed + // Use ISender (not IMediator) — exposes only Send/CreateStream + + private static async Task>> GetAllOrders( + ISender sender, CancellationToken ct) + => TypedResults.Ok(await sender.Send(new GetAllOrdersQuery(), ct)); + + private static async Task, NotFound>> GetOrder( + Guid id, ISender sender, CancellationToken ct) + { + var result = await sender.Send(new GetOrderQuery(id), ct); + return result is not null ? TypedResults.Ok(result) : TypedResults.NotFound(); + } + + private static async Task, ValidationProblem>> CreateOrder( + CreateOrderCommand command, ISender sender, CancellationToken ct) + { + try + { + var order = await sender.Send(command, ct); + return TypedResults.Created($"/orders/{order.Id}", order); + } + catch (ValidationException ex) + { + return TypedResults.ValidationProblem(ex.ToDictionary()); + } + } + + private static async Task> CancelOrder( + Guid id, ISender sender, CancellationToken ct) + { + var cancelled = await sender.Send(new CancelOrderCommand(id), ct); + return cancelled ? TypedResults.NoContent() : TypedResults.NotFound(); + } +} +``` + +### 4.2 InfrastructureExtensions + +```csharp +// Infrastructure/Extensions/InfrastructureExtensions.cs +namespace MyApi.Infrastructure.Extensions; + +public static class InfrastructureExtensions +{ + public static IServiceCollection AddInfrastructure( + this IServiceCollection services, + IConfiguration configuration) + { + // ── Database ────────────────────────────────────────────────────── + services.AddDbContext(options => + options.UseNpgsql( + configuration.GetConnectionString("DefaultConnection") + ?? throw new InvalidOperationException( + "Connection string 'DefaultConnection' not found."))); + + // ── External services ───────────────────────────────────────────── + services.AddHttpClient(client => + { + client.BaseAddress = new Uri( + configuration["ExternalService:BaseUrl"] + ?? throw new InvalidOperationException("ExternalService:BaseUrl not configured.")); + }); + + services.AddScoped(); + + return services; + } +} +``` + +### 4.3 AppDbContext + +```csharp +// Infrastructure/Persistence/AppDbContext.cs +namespace MyApi.Infrastructure.Persistence; + +public class AppDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Orders => Set(); + public DbSet Users => Set(); + public DbSet Products => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // Auto-discovers all IEntityTypeConfiguration classes in the assembly + modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly); + } +} +``` + +### 4.4 Entity Configuration + +```csharp +// Infrastructure/Persistence/Configurations/OrderConfiguration.cs +namespace MyApi.Infrastructure.Persistence.Configurations; + +public sealed class OrderConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("orders"); + + builder.HasKey(o => o.Id); + + builder.Property(o => o.Id) + .HasDefaultValueSql("gen_random_uuid()"); + + builder.Property(o => o.Status) + .HasConversion() + .HasMaxLength(50) + .IsRequired(); + + builder.Property(o => o.CreatedAt) + .HasDefaultValueSql("now()") + .IsRequired(); + + builder.HasQueryFilter(o => !o.IsDeleted); + + builder.HasOne(o => o.User) + .WithMany(u => u.Orders) + .HasForeignKey(o => o.UserId) + .OnDelete(DeleteBehavior.Restrict); + } +} +``` + +--- + +## 6. EF Core & PostgreSQL + +### 5.1 Connection String (appsettings.json) + +```json +{ + "ConnectionStrings": { + "DefaultConnection": "Host=localhost;Port=5432;Database=myapp;Username=postgres;Password=yourpassword" + } +} +``` + +Override in environment / Docker / Kubernetes using double-underscore notation: +``` +ConnectionStrings__DefaultConnection=Host=prod-db;... +``` + +For local development, use `dotnet user-secrets`: +```bash +dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Host=localhost;..." +``` + +### 5.2 Async Query Patterns + +```csharp +// ✅ Always pass CancellationToken to all EF async methods +var orders = await db.Orders + .AsNoTracking() // read-only queries: skip change tracking + .Where(o => o.UserId == userId) + .OrderByDescending(o => o.CreatedAt) + .ToListAsync(ct); + +// ✅ FindAsync for primary key lookup (uses change tracker cache first) +var order = await db.Orders.FindAsync([orderId], ct); + +// ✅ Bulk update / delete without loading entities (EF 7+) +await db.Orders + .Where(o => o.Status == OrderStatus.Pending && o.CreatedAt < cutoff) + .ExecuteUpdateAsync(s => s.SetProperty(o => o.Status, OrderStatus.Expired), ct); + +await db.Orders + .Where(o => o.IsDeleted && o.CreatedAt < cutoff) + .ExecuteDeleteAsync(ct); +``` + +### 5.3 AsNoTracking + +```csharp +// ✅ Use AsNoTracking for all read/query handlers — no change tracker overhead +var result = await db.Orders + .AsNoTracking() + .Where(o => o.Id == id) + .Select(o => new OrderResult(o.Id, o.UserId, o.Status, o.CreatedAt)) + .FirstOrDefaultAsync(ct); + +// ✅ Omit AsNoTracking in command handlers that modify and call SaveChangesAsync +var order = await db.Orders.FindAsync([id], ct); +order!.Status = OrderStatus.Shipped; +await db.SaveChangesAsync(ct); +``` + +--- + +## 7. Migrations + +### Common Commands + +```bash +# Add a new migration after model changes +dotnet ef migrations add MigrationName + +# Apply pending migrations to the database +dotnet ef database update + +# List all migrations and their applied status +dotnet ef migrations list + +# Remove the last unapplied migration +dotnet ef migrations remove + +# Multi-project setup (DbContext in separate library) +dotnet ef migrations add MigrationName \ + --project src/MyApp.Data \ + --startup-project src/MyApp.Api +``` + +### Production Deployment + +```bash +# Generate idempotent SQL script — safe to run multiple times (recommended for CI/CD) +dotnet ef migrations script --idempotent --output migrations.sql + +# EF 9: self-contained migration bundle (no dotnet SDK needed at deploy time) +dotnet ef migrations bundle --output migrations-bundle +./migrations-bundle --connection "${DB_CONNECTION_STRING}" +``` + +> **Do not** auto-migrate in `Program.cs` (`db.Database.MigrateAsync()`) in production multi-instance deployments — use SQL scripts or migration bundles instead to avoid race conditions. + +--- + +## 8. NuGet Packages + +```xml + + + + + + + + + + + + + + + + + + + + + + + +``` + +--- + +## GlobalUsings.cs + +```csharp +global using System; +global using System.Collections.Generic; +global using System.Linq; +global using System.Threading; +global using System.Threading.Tasks; +global using FluentValidation; +global using MediatR; +global using Microsoft.AspNetCore.Http.HttpResults; +global using Microsoft.AspNetCore.Routing; +global using Microsoft.EntityFrameworkCore; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Logging; +global using MyApi.Infrastructure.Persistence; +global using NSubstitute; +``` + +--- + +## Quick Reference + +| Concern | Where it lives | Rule | +|---------|---------------|------| +| DI wiring + endpoint mapping | `Program.cs` | Declarative only, no logic | +| API entry points (discover processes) | `Api/{Name}Endpoints.cs` | `IEndpointRouteBuilder` extension, routing + dispatch only | +| Business use case | `Features/{Name}/{UseCase}.cs` | Command/Query + Validator + Handler only | +| API request/response contract | Command/Query record + result record | No separate DTOs, no mapping | +| Pipeline behaviors | `Features/Common/Behaviors/` | `IPipelineBehavior` | +| Shared exceptions | `Features/Common/Exceptions/` | Domain exception hierarchy | +| Database context + entity configs | `Infrastructure/Persistence/` | `AppDbContext` + `IEntityTypeConfiguration` | +| EF Core migrations | `Infrastructure/Persistence/Migrations/` | Auto-generated by `dotnet ef` | +| External services | `Infrastructure/Services/` | Interface + implementation | +| Infra DI registration | `Infrastructure/Extensions/InfrastructureExtensions.cs` | `AddInfrastructure()` | +| Global usings | `GlobalUsings.cs` | `global using` | diff --git a/.opencode/context/core/standards/csharp.md b/.opencode/context/core/standards/csharp.md new file mode 100644 index 0000000..ada8e1d --- /dev/null +++ b/.opencode/context/core/standards/csharp.md @@ -0,0 +1,903 @@ +--- +id: csharp +name: C# Standards +description: "Universal C# best practices for naming, async, LINQ, error handling, and testing" +category: core +type: standard +version: 1.1.0 +author: community +--- + + + +# Universal C# Standards + +**Purpose**: Universal C# best practices for AI agents working on .NET projects +**Scope**: Language-level patterns, not framework-specific +**Last Updated**: 2026-03-15 + +--- + +## Table of Contents + +1. [Naming Conventions](#1-naming-conventions) +2. [Type Safety & Nullability](#2-type-safety--nullability) +3. [Async/Await Patterns](#3-asyncawait-patterns) +4. [LINQ](#4-linq) +5. [Error Handling](#5-error-handling) +6. [Pattern Matching](#6-pattern-matching) +7. [Code Organization](#7-code-organization) +8. [Records & Immutability](#8-records--immutability) +9. [Dependency Injection](#9-dependency-injection) +10. [Testing](#10-testing) + +--- + +## 1. Naming Conventions + +### 1.1 General Rules + +| Element | Convention | Example | +|---------|-----------|---------| +| Classes, structs, records | PascalCase | `UserService`, `OrderItem` | +| Interfaces | `I` + PascalCase | `IUserRepository`, `IOrderService` | +| Methods | PascalCase | `GetUserById`, `ProcessOrder` | +| Properties | PascalCase | `FirstName`, `CreatedAt` | +| Fields (private) | `_` + camelCase | `_logger`, `_repository` | +| Local variables | camelCase | `userId`, `orderTotal` | +| Parameters | camelCase | `userId`, `cancellationToken` | +| Constants | PascalCase | `MaxRetryCount`, `DefaultTimeout` | +| Enums | PascalCase (type and members) | `OrderStatus.Pending` | +| Async methods | `Async` suffix | `GetUserAsync`, `SaveOrderAsync` | + +### 1.2 Method Naming + +```csharp +// ✅ GOOD - Verb + noun, PascalCase +public async Task GetUserByIdAsync(Guid userId) { } +public async Task> ListOrdersAsync(Guid userId) { } +public async Task DeleteUserAsync(Guid userId) { } +public bool IsEligibleForDiscount(Order order) { } +public bool HasPermission(string action) { } + +// ❌ AVOID - Ambiguous or wrong case +public async Task fetchuser(Guid id) { } +public async Task DoUserGet(Guid id) { } +``` + +### 1.3 Interface Naming + +```csharp +// ✅ GOOD - Always prefix with I +public interface IUserRepository { } +public interface IOrderService { } +public interface INotificationSender { } + +// ❌ AVOID - No prefix, or wrong prefix +public interface UserRepository { } +public interface TUserRepository { } +``` + +### 1.4 Private Fields + +```csharp +// ✅ GOOD - Underscore prefix, camelCase +public class OrderService +{ + private readonly IOrderRepository _orderRepository; + private readonly ILogger _logger; + private int _retryCount; +} + +// ❌ AVOID - No prefix, or m_ prefix +private IOrderRepository orderRepository; +private IOrderRepository m_orderRepository; +``` + +--- + +## 2. Type Safety & Nullability + +### 2.1 Enable Nullable Reference Types + +**Rule: Always enable nullable reference types in all projects** + +```xml + + + enable + nullable + +``` + +```csharp +// ✅ GOOD - Explicit nullability +public string Name { get; set; } // Non-nullable: must be assigned +public string? MiddleName { get; set; } // Nullable: may be null + +public User? FindUser(Guid id) { } // Returns null if not found +public User GetUser(Guid id) { } // Never returns null (throws if not found) +``` + +### 2.2 Null Handling + +```csharp +// ✅ GOOD - Null-coalescing and conditional operators +var name = user?.Name ?? "Unknown"; +var city = user?.Address?.City ?? string.Empty; +user?.Notify("Welcome"); + +// ✅ GOOD - Null guard at method entry +public void ProcessOrder(Order order) +{ + ArgumentNullException.ThrowIfNull(order); + // ... +} + +// ✅ GOOD - Null-coalescing assignment +_cache ??= new Dictionary(); + +// ❌ AVOID - Manual null checks where operators suffice +if (user != null && user.Address != null) + city = user.Address.City; +``` + +### 2.3 Avoid Primitive Obsession + +```csharp +// ✅ GOOD - Strongly typed IDs prevent mixing up parameters +public readonly record struct UserId(Guid Value); +public readonly record struct OrderId(Guid Value); + +public Task GetOrderAsync(OrderId orderId, UserId userId) { } + +// ❌ AVOID - Raw Guids are easy to mix up +public Task GetOrderAsync(Guid orderId, Guid userId) { } +``` + +--- + +## 3. Async/Await Patterns + +### 3.1 Always Use CancellationToken + +**Rule: Every public async method must accept a CancellationToken** + +```csharp +// ✅ GOOD - CancellationToken flows through all async calls +public async Task GetUserAsync(Guid userId, CancellationToken cancellationToken = default) +{ + var user = await _repository.FindAsync(userId, cancellationToken); + return user ?? throw new NotFoundException($"User {userId} not found"); +} + +// ❌ AVOID - No way to cancel long-running operations +public async Task GetUserAsync(Guid userId) +{ + return await _repository.FindAsync(userId); +} +``` + +### 10.6 When to Use Moq Instead of NSubstitute + +While **NSubstitute is the default**, use **Moq** for: + +```csharp +// Use Moq when you need MockBehavior.Strict (fail on unexpected calls) +var mockRepository = new Mock(MockBehavior.Strict); +mockRepository.Setup(r => r.GetOrderAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new Order()); + +// Use Moq for verifying complex call sequences +var sequence = new MockSequence(); +_serviceA.InSequence(sequence).Setup(x => x.MethodA()).ReturnsAsync(true); +_serviceB.InSequence(sequence).Setup(x => x.MethodB()).ReturnsAsync(false); + +// Use Moq for verifying no other calls were made +mock.VerifyNoOtherCalls(); + +// Use Moq for mocking protected members +mock.Protected().Setup("GetName").Returns("test"); +``` + +**Note**: NSubstitute covers ~95% of real-world test scenarios. Only reach for Moq's advanced features if NSubstitute doesn't provide what you need. + +--- + +## 4. LINQ + +### 4.1 Prefer Method Syntax for Simple Chains + +```csharp +// ✅ GOOD - Method syntax for filter/project/sort +var activeUserNames = users + .Where(u => u.IsActive) + .OrderBy(u => u.LastName) + .Select(u => u.FullName) + .ToList(); + +// ✅ GOOD - Query syntax for complex joins (more readable) +var result = + from order in orders + join user in users on order.UserId equals user.Id + where order.Status == OrderStatus.Pending + select new { order.Id, user.Name }; +``` + +### 4.2 Materialize at the Right Time + +```csharp +// ✅ GOOD - Materialize once, use the list +var activeUsers = users.Where(u => u.IsActive).ToList(); +var count = activeUsers.Count; +var first = activeUsers.FirstOrDefault(); + +// ❌ AVOID - Multiple enumerations of IEnumerable (re-evaluates query each time) +var activeUsers = users.Where(u => u.IsActive); +var count = activeUsers.Count(); // evaluates query +var first = activeUsers.First(); // evaluates query again +``` + +### 4.3 Use Appropriate Termination Methods + +```csharp +// ✅ GOOD - Choose the right method for the intent +var first = items.FirstOrDefault(); // null if empty +var single = items.SingleOrDefault(); // null if empty, throws if >1 +var any = items.Any(x => x.IsActive); // bool, stops at first match +var all = items.All(x => x.IsActive); // bool, fails fast on false +var count = items.Count(x => x.IsActive); // full enumeration + +// ❌ AVOID - Using Count() to check existence (full enumeration) +if (items.Count() > 0) { } // Use Any() instead +``` + +### 4.4 Avoid LINQ in Performance-Critical Paths + +```csharp +// ✅ GOOD - Direct loop when allocation matters +var total = 0m; +foreach (var item in orderItems) + total += item.Price * item.Quantity; + +// LINQ alternative (fine for most code, avoids premature optimization) +var total = orderItems.Sum(item => item.Price * item.Quantity); +``` + +--- + +## 5. Error Handling + +### 5.1 Use Specific Exception Types + +```csharp +// ✅ GOOD - Specific, meaningful exceptions +public async Task GetUserAsync(Guid userId, CancellationToken ct = default) +{ + var user = await _repository.FindAsync(userId, ct); + if (user is null) + throw new NotFoundException($"User '{userId}' was not found."); + return user; +} + +// ✅ GOOD - Domain exception hierarchy +public class DomainException : Exception +{ + public DomainException(string message) : base(message) { } + public DomainException(string message, Exception inner) : base(message, inner) { } +} + +public class NotFoundException : DomainException +{ + public NotFoundException(string message) : base(message) { } +} + +public class ValidationException : DomainException +{ + public IReadOnlyList Errors { get; } + + public ValidationException(IReadOnlyList errors) + : base("One or more validation errors occurred.") + => Errors = errors; +} + +// ❌ AVOID - Generic exceptions with no context +throw new Exception("Not found"); +throw new ApplicationException("Something went wrong"); +``` + +### 5.2 Validate at Entry Points + +```csharp +// ✅ GOOD - Fail fast with guard clauses +public void PlaceOrder(Order order, Guid userId) +{ + ArgumentNullException.ThrowIfNull(order); + ArgumentNullException.ThrowIfNull(userId); + + if (order.Items.Count == 0) + throw new ValidationException(["Order must contain at least one item."]); + + if (order.TotalAmount <= 0) + throw new ValidationException(["Order total must be greater than zero."]); + + // proceed with valid input +} +``` + +### 5.3 Result Pattern (for Expected Failures) + +```csharp +// ✅ GOOD - Use Result when failure is a normal outcome (not exceptional) +public readonly record struct Result +{ + public bool IsSuccess { get; } + public T? Value { get; } + public string? Error { get; } + + private Result(bool isSuccess, T? value, string? error) + { + IsSuccess = isSuccess; + Value = value; + Error = error; + } + + public static Result Success(T value) => new(true, value, null); + public static Result Failure(string error) => new(false, default, error); +} + +// Usage +public Result SubmitOrder(Cart cart) +{ + if (!cart.HasItems) + return Result.Failure("Cart is empty."); + + var order = CreateOrderFromCart(cart); + return Result.Success(order); +} + +var result = SubmitOrder(cart); +if (result.IsSuccess) + Console.WriteLine($"Order created: {result.Value!.Id}"); +else + Console.WriteLine($"Failed: {result.Error}"); + +// Reserve exceptions for truly exceptional, unexpected conditions. +// Use Result for expected business failures (validation, not found in context of search, etc.) +``` + +### 5.4 Catch Specific Exceptions + +```csharp +// ✅ GOOD - Catch what you can handle +try +{ + await _paymentGateway.ChargeAsync(amount, ct); +} +catch (PaymentDeclinedException ex) +{ + _logger.LogWarning(ex, "Payment declined for amount {Amount}", amount); + return Result.Failure("Payment was declined."); +} +catch (TimeoutException ex) +{ + _logger.LogError(ex, "Payment gateway timed out"); + throw; // re-throw: let caller or middleware handle retries +} + +// ❌ AVOID - Swallowing exceptions silently +try { await _paymentGateway.ChargeAsync(amount, ct); } +catch { } + +// ❌ AVOID - Catching Exception broadly without re-throw +catch (Exception ex) +{ + _logger.LogError(ex, "Error"); + return null; // hides the real problem +} +``` + +--- + +## 6. Pattern Matching + +### 6.1 Switch Expressions (Prefer over switch statements) + +```csharp +// ✅ GOOD - Switch expression: concise, exhaustive +public decimal GetDiscount(CustomerTier tier) => tier switch +{ + CustomerTier.Bronze => 0.00m, + CustomerTier.Silver => 0.05m, + CustomerTier.Gold => 0.10m, + CustomerTier.Platinum => 0.20m, + _ => throw new ArgumentOutOfRangeException(nameof(tier), tier, null) +}; + +// ❌ AVOID - Verbose switch statement for simple value mapping +switch (tier) +{ + case CustomerTier.Bronze: return 0.00m; + case CustomerTier.Silver: return 0.05m; + // ... +} +``` + +### 6.2 Type Patterns + +```csharp +// ✅ GOOD - Type pattern with declaration +public string Describe(Shape shape) => shape switch +{ + Circle c => $"Circle with radius {c.Radius}", + Rectangle r => $"Rectangle {r.Width}x{r.Height}", + Triangle t => $"Triangle with base {t.Base}", + _ => "Unknown shape" +}; + +// ✅ GOOD - is pattern for type checking with binding +if (notification is EmailNotification email) +{ + await SendEmailAsync(email.Address, email.Body, ct); +} +``` + +### 6.3 Property Patterns + +```csharp +// ✅ GOOD - Property pattern for readable conditionals +public decimal CalculateShipping(Order order) => order switch +{ + { TotalAmount: >= 100 } => 0m, // free shipping + { IsExpressDelivery: true } => 15m, // express + { ShippingAddress.Country: "FI" } => 5m, // domestic + _ => 10m // international +}; +``` + +### 6.4 Deconstruction + +```csharp +// ✅ GOOD - Deconstruct tuples and records +var (firstName, lastName) = GetFullName(userId); +var (lat, lon) = location; + +// ✅ GOOD - Discard unused parts +var (id, _, createdAt) = GetOrderSummary(orderId); +``` + +--- + +## 7. Code Organization + +### 7.1 Namespace Per Feature (not per type) + +```csharp +// ✅ GOOD - Feature-based namespaces +namespace MyApp.Orders; // all order-related types together +namespace MyApp.Users; // all user-related types together +namespace MyApp.Notifications; + +// ❌ AVOID - Layer-based namespaces that scatter a feature across the codebase +namespace MyApp.Repositories; +namespace MyApp.Services; +namespace MyApp.Controllers; +``` + +### 7.2 File-Scoped Namespaces + +```csharp +// ✅ GOOD - File-scoped namespace (C# 10+): less indentation +namespace MyApp.Orders; + +public class OrderService { } + +// ❌ AVOID - Block-scoped namespace adds unnecessary indentation +namespace MyApp.Orders +{ + public class OrderService { } +} +``` + +### 7.3 One Type Per File + +```csharp +// ✅ GOOD - OrderService.cs contains only OrderService +// ✅ ACCEPTABLE - Small, closely related types in one file (e.g., value objects + their exceptions) + +// ❌ AVOID - Multiple unrelated types in one file +// OrderService.cs containing OrderService + UserService + ProductRepository +``` + +### 7.4 Using Directives + +```csharp +// ✅ GOOD - Global usings for commonly used namespaces (in a GlobalUsings.cs file) +global using System; +global using System.Collections.Generic; +global using System.Threading; +global using System.Threading.Tasks; +global using Microsoft.Extensions.Logging; + +// ✅ GOOD - File-level usings at the top, outside namespace +using System.Text.Json; +using MyApp.Common; + +namespace MyApp.Orders; +``` + +### 7.5 Class Structure Order + +Follow this order within a class: + +```csharp +public class OrderService : IOrderService +{ + // 1. Constants + private const int MaxRetryCount = 3; + + // 2. Static fields + private static readonly JsonSerializerOptions JsonOptions = new(); + + // 3. Instance fields (private) + private readonly IOrderRepository _repository; + private readonly ILogger _logger; + + // 4. Constructor(s) + public OrderService(IOrderRepository repository, ILogger logger) + { + _repository = repository; + _logger = logger; + } + + // 5. Properties + + // 6. Public methods + + // 7. Private methods +} +``` + +--- + +## 8. Records & Immutability + +### 8.1 Use Records for Value Objects and DTOs + +```csharp +// ✅ GOOD - Record for immutable data (value semantics, built-in equality) +public record UserDto(Guid Id, string Name, string Email); + +public record Address(string Street, string City, string PostalCode, string Country); + +// ✅ GOOD - Readonly record struct for small value objects (stack allocated) +public readonly record struct Money(decimal Amount, string Currency); +public readonly record struct Coordinates(double Latitude, double Longitude); + +// Non-destructive mutation via 'with' +var updated = originalUser with { Email = "new@example.com" }; +``` + +### 8.2 Immutable Collections + +```csharp +// ✅ GOOD - Expose immutable views +public class Order +{ + private readonly List _items = new(); + + public IReadOnlyList Items => _items.AsReadOnly(); + + public void AddItem(OrderItem item) + { + ArgumentNullException.ThrowIfNull(item); + _items.Add(item); + } +} + +// ✅ GOOD - ImmutableList for truly immutable scenarios +using System.Collections.Immutable; + +public record ShoppingCart(ImmutableList Items) +{ + public ShoppingCart AddItem(CartItem item) => + this with { Items = Items.Add(item) }; + + public ShoppingCart RemoveItem(Guid itemId) => + this with { Items = Items.RemoveAll(i => i.Id == itemId) }; +} +``` + +### 8.3 init-only Properties + +```csharp +// ✅ GOOD - init allows construction but prevents later mutation +public class OrderConfiguration +{ + public Guid OrderId { get; init; } + public string Currency { get; init; } = "EUR"; + public int MaxItems { get; init; } = 100; +} + +// Can set during object initializer, but not after +var config = new OrderConfiguration { OrderId = Guid.NewGuid(), Currency = "USD" }; +// config.Currency = "EUR"; // ❌ compile error +``` + +--- + +## 9. Dependency Injection + +### 9.1 Constructor Injection (Preferred) + +```csharp +// ✅ GOOD - All dependencies injected through constructor, stored readonly +public class OrderService : IOrderService +{ + private readonly IOrderRepository _repository; + private readonly IPaymentGateway _paymentGateway; + private readonly ILogger _logger; + + public OrderService( + IOrderRepository repository, + IPaymentGateway paymentGateway, + ILogger logger) + { + _repository = repository; + _paymentGateway = paymentGateway; + _logger = logger; + } +} + +// ❌ AVOID - Service locator pattern (hidden dependencies) +public class OrderService +{ + public async Task ProcessAsync() + { + var repo = ServiceLocator.Get(); // hidden dependency + } +} +``` + +### 9.2 Lifetime Registration + +```csharp +// Registration in Program.cs or an extension method +services.AddScoped(); // per HTTP request +services.AddTransient(); // new instance each time +services.AddSingleton(); // single instance + +// ✅ GOOD - Extension method groups related registrations +public static class OrdersServiceCollectionExtensions +{ + public static IServiceCollection AddOrders(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + return services; + } +} + +// In Program.cs +builder.Services.AddOrders(); +``` + +### 9.3 Options Pattern for Configuration + +```csharp +// ✅ GOOD - Strongly-typed configuration +public class PaymentOptions +{ + public const string SectionName = "Payment"; + + public string ApiKey { get; set; } = string.Empty; + public string BaseUrl { get; set; } = string.Empty; + public int TimeoutSeconds { get; set; } = 30; +} + +// Registration +builder.Services.Configure( + builder.Configuration.GetSection(PaymentOptions.SectionName)); + +// Usage +public class PaymentGateway +{ + private readonly PaymentOptions _options; + + public PaymentGateway(IOptions options) + { + _options = options.Value; + } +} +``` + +--- + +## 10. Testing + +### 10.1 Framework & Structure + +**Use xUnit** as the default test framework. Use **Shouldly** for readable assertions. Use **NSubstitute** for mocking (pragmatic, readable syntax). Use **Moq** only for scenarios requiring strict mock behavior or complex verification. + +```csharp +// ✅ GOOD - xUnit test class structure with NSubstitute +public class OrderServiceTests +{ + // Arrange shared fixtures in constructor or use class fixtures + private readonly IOrderRepository _repositorySubstitute = Substitute.For(); + private readonly ILogger _loggerSubstitute = Substitute.For>(); + private readonly OrderService _sut; + + public OrderServiceTests() + { + _sut = new OrderService(_repositorySubstitute, _loggerSubstitute); + } + + [Fact] + public async Task GetOrderAsync_WhenOrderExists_ReturnsOrder() + { + // Arrange + var orderId = Guid.NewGuid(); + var expected = new Order { Id = orderId }; + _repositorySubstitute + .FindAsync(orderId, Arg.Any()) + .Returns(Task.FromResult(expected)); + + // Act + var result = await _sut.GetOrderAsync(orderId); + + // Assert + result.ShouldBeEquivalentTo(expected); + } + + [Fact] + public async Task GetOrderAsync_WhenOrderNotFound_ThrowsNotFoundException() + { + // Arrange + var orderId = Guid.NewGuid(); + _repositorySubstitute + .FindAsync(orderId, Arg.Any()) + .Returns(Task.FromResult(null)); + + // Act + var act = () => _sut.GetOrderAsync(orderId); + + // Assert + await act.ShouldThrowAsync(); + } + + [Fact] + public async Task SaveOrderAsync_CallsRepository_VerifiesInteraction() + { + // Arrange + var order = new Order { Id = Guid.NewGuid() }; + + // Act + await _sut.SaveOrderAsync(order); + + // Assert + await _repositorySubstitute.Received(1).SaveAsync(order, Arg.Any()); + } +} +``` + +**Why NSubstitute?** +- ✅ Fluent, readable syntax — substitutes ARE the interfaces, no `.Object` indirection +- ✅ Lower ceremony, less noise in tests +- ✅ `Arg.Any()` is cleaner than `It.IsAny()` +- ✅ `Received()` reads naturally as "verify it received this call" +- ✅ Faster to write and understand, even for complex scenarios + +### 10.2 Parameterized Tests + +```csharp +// ✅ GOOD - Theory with InlineData for multiple cases +[Theory] +[InlineData(CustomerTier.Bronze, 0.00)] +[InlineData(CustomerTier.Silver, 0.05)] +[InlineData(CustomerTier.Gold, 0.10)] +[InlineData(CustomerTier.Platinum, 0.20)] +public void GetDiscount_ReturnsCorrectRate(CustomerTier tier, decimal expected) +{ + var result = _sut.GetDiscount(tier); + result.ShouldBe(expected); +} + +// ✅ GOOD - MemberData for complex input objects +public static IEnumerable InvalidOrders => +[ + [new Order { Items = [] }, "Order must contain at least one item."], + [new Order { Items = [item], TotalAmount = -1 }, "Order total must be greater than zero."], +]; + +[Theory] +[MemberData(nameof(InvalidOrders))] +public void PlaceOrder_WithInvalidOrder_ThrowsValidationException(Order order, string expectedError) +{ + var act = () => _sut.PlaceOrder(order, Guid.NewGuid()); + act.ShouldThrow() + .Errors.ShouldContain(expectedError); +} +``` + +### 10.3 Test Naming + +```csharp +// ✅ GOOD - MethodName_StateUnderTest_ExpectedBehavior +public async Task GetOrderAsync_WhenOrderExists_ReturnsOrder() { } +public async Task GetOrderAsync_WhenOrderNotFound_ThrowsNotFoundException() { } +public void PlaceOrder_WithEmptyCart_ThrowsValidationException() { } +public void CalculateDiscount_ForPlatinumCustomer_Returns20Percent() { } +``` + +### 10.4 Avoid Logic in Tests + +```csharp +// ✅ GOOD - Direct, no conditionals or loops +[Fact] +public void FormatName_ReturnsFullName() +{ + var user = new User { FirstName = "John", LastName = "Doe" }; + var result = _sut.FormatName(user); + result.ShouldBe("John Doe"); +} + +// ❌ AVOID - Logic in tests (makes failures hard to diagnose) +[Fact] +public void FormatNames_ReturnsFullNames() +{ + var users = GetTestUsers(); + foreach (var user in users) + { + if (user.FirstName != null) + _sut.FormatName(user).ShouldContain(user.FirstName); + } +} +``` + +### 10.5 Integration Tests + +```csharp +// ✅ GOOD - Use WebApplicationFactory for ASP.NET Core integration tests +public class OrdersApiTests : IClassFixture> +{ + private readonly HttpClient _client; + + public OrdersApiTests(WebApplicationFactory factory) + { + _client = factory.WithWebHostBuilder(builder => + { + builder.ConfigureServices(services => + { + // Replace real DB with in-memory for tests + services.RemoveAll(); + services.AddDbContext(o => o.UseInMemoryDatabase("TestDb")); + }); + }).CreateClient(); + } + + [Fact] + public async Task GetOrder_ReturnsOk() + { + var response = await _client.GetAsync("/api/orders/123"); + response.StatusCode.ShouldBe(HttpStatusCode.OK); + } +} +``` + +--- + +## Quick Reference + +| Topic | Key Rule | +|-------|---------| +| Naming | PascalCase methods/properties, `_camelCase` fields, `I`-prefix interfaces, `Async` suffix | +| Nullability | Enable `enable`, use `?` explicitly, guard with `ArgumentNullException.ThrowIfNull` | +| Async | Always pass `CancellationToken`, avoid `async void`, prefer `Task.WhenAll` for parallel ops | +| LINQ | Materialize with `ToList()` once, use `Any()` not `Count() > 0` | +| Errors | Specific exception types, validate at entry, `Result` for expected failures | +| Pattern matching | Switch expressions over switch statements, property patterns for readable conditionals | +| Organization | File-scoped namespaces, feature-based folders, one type per file | +| Immutability | Records for value objects/DTOs, `IReadOnlyList` for exposed collections | +| DI | Constructor injection, `readonly` fields, Options pattern for config | +| Testing | xUnit + Shouldly, `[Fact]`/`[Theory]`, Arrange-Act-Assert, no logic in tests | diff --git a/.opencode/context/core/standards/navigation.md b/.opencode/context/core/standards/navigation.md index 6831ab1..f7e83e1 100644 --- a/.opencode/context/core/standards/navigation.md +++ b/.opencode/context/core/standards/navigation.md @@ -17,6 +17,9 @@ | `project-intelligence.md` | What and why | ⭐⭐⭐⭐ | Onboarding, understanding projects | | `project-intelligence-management.md` | How to manage | ⭐⭐⭐ | Managing intelligence files | | `code-analysis.md` | Analysis approaches | ⭐⭐⭐ | Analyzing code, debugging | +| `typescript.md` | Universal TypeScript patterns | ⭐⭐⭐⭐ | Writing/reviewing TypeScript code | +| `csharp.md` | Universal C# / .NET patterns | ⭐⭐⭐⭐ | Writing/reviewing C# code | +| `csharp-project-structure.md` | ASP.NET Core project structure (Minimal APIs, CQRS, EF Core + PostgreSQL) | ⭐⭐⭐⭐ | Starting or structuring a C# API project | --- @@ -26,6 +29,18 @@ 1. Load `code-quality.md` (critical) 2. Load `security-patterns.md` (high) +**For TypeScript code**: +1. Load `typescript.md` (critical) +2. Load `code-quality.md` (high) + +**For C# / .NET code**: +1. Load `csharp.md` (critical) +2. Load `code-quality.md` (high) + +**For C# API project structure**: +1. Load `csharp-project-structure.md` (critical) +2. Load `csharp.md` (high) + **For testing**: 1. Load `test-coverage.md` (critical) 2. Depends on: `code-quality.md` diff --git a/README.md b/README.md index b010cf5..500b28e 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ 📝 **Editable Agents** - Full control over AI behavior 👥 **Team-Ready** - Everyone uses the same patterns -**Multi-language:** TypeScript • Python • Go • Rust • Any language* +**Multi-language:** TypeScript • Python • Go • Rust • C# • Any language* **Model Agnostic:** Claude • GPT • Gemini • Local models @@ -684,7 +684,7 @@ Build complete custom AI systems tailored to your domain in minutes. Interactive A: Yes! Use Git Bash (recommended) or WSL. **Q: What languages are supported?** -A: Agents are language-agnostic and adapt based on your project files. Primarily tested with TypeScript/Node.js. Python, Go, Rust, and other languages are supported but less battle-tested. The context system works with any language. +A: Agents are language-agnostic and adapt based on your project files. Primarily tested with TypeScript/Node.js. C# / .NET is now supported with dedicated context files. Python, Go, Rust, and other languages are supported but less battle-tested. The context system works with any language. **Q: Do I need to add context?** A: No, but it's highly recommended. Without context, agents write generic code. With context, they write YOUR code. @@ -767,7 +767,7 @@ Check out our [**Project Board**](https://github.com/darrenhinde/OpenAgentsContr - **Plugin System** - npm-based plugin architecture for easy distribution - **Performance Improvements** - Faster agent execution and context loading - **Enhanced Context Discovery** - Smarter pattern recognition -- **Multi-language Support** - Better Python, Go, Rust support +- **Multi-language Support** - Better Python, Go, Rust, C# / .NET support - **Team Collaboration** - Shared context and team workflows - **Documentation** - More examples, tutorials, and guides diff --git a/docs/agents/opencoder.md b/docs/agents/opencoder.md index 605619f..2fad268 100644 --- a/docs/agents/opencoder.md +++ b/docs/agents/opencoder.md @@ -25,7 +25,7 @@ OpenCoder is a **specialized development agent** focused on complex coding tasks **Key Characteristics:** - 🎯 **Specialized** - Deep focus on code quality and architecture -- 🔧 **Multi-language** - Adapts to TypeScript, Python, Go, Rust, and more +- 🔧 **Multi-language** - Adapts to TypeScript, Python, Go, Rust, C#, and more - 📐 **Plan-first** - Always proposes plans before implementation - 🏗️ **Modular** - Emphasizes clean architecture and separation of concerns - ✅ **Quality-focused** - Includes testing, type checking, and validation @@ -101,7 +101,7 @@ Use **openagent** for: - Technical debt reduction ### Quality Assurance -- Type checking (TypeScript, Python, Go, Rust) +- Type checking (TypeScript, Python, Go, Rust, C#) - Linting (ESLint, Pylint, etc.) - Build validation - Test execution @@ -190,6 +190,12 @@ OpenCoder adapts to the project's language automatically: - Linting: `clippy` - Testing: `cargo test` +### C# / .NET +- Runtime: `dotnet run` +- Type checking: built-in (Roslyn compiler) +- Linting: `dotnet format`, Roslyn analyzers +- Testing: `dotnet test` (xUnit + Shouldly + NSubstitute) + --- ## Code Standards @@ -384,6 +390,7 @@ OpenCoder adapts to your language: - For Python: Pythonic patterns, type hints - For Go: Idiomatic Go, interfaces - For Rust: Ownership, traits, Result types +- For C#: Minimal APIs, CQRS (MediatR), records, async/await, nullable reference types ---