# C#/.NET-Specific Guidelines

This document contains C# and .NET-specific instructions for AO projects. Reference this alongside the main `copilot-instructions.md`.

## Testing Framework: xUnit/NUnit

### Preferred Test Structure (xUnit)
```csharp
// Tests/FeatureTests.cs
using Xunit;
using FluentAssertions;

public class FeatureTests : IDisposable
{
    private readonly FeatureClass _sut;  // System Under Test
    private readonly string _tempDir;
    
    public FeatureTests()
    {
        _tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
        Directory.CreateDirectory(_tempDir);
        _sut = new FeatureClass();
    }
    
    public void Dispose()
    {
        if (Directory.Exists(_tempDir))
            Directory.Delete(_tempDir, recursive: true);
    }
    
    [Fact]
    public void Process_ValidInput_ReturnsExpectedResult()
    {
        var result = _sut.Process("input");
        
        result.Should().Be("expected");
    }
    
    [Theory]
    [InlineData("input1", "output1")]
    [InlineData("input2", "output2")]
    public void Process_MultipleInputs_ReturnsCorrectOutput(string input, string expected)
    {
        var result = _sut.Process(input);
        
        result.Should().Be(expected);
    }
    
    [Fact]
    public void Process_NullInput_ThrowsArgumentNullException()
    {
        var act = () => _sut.Process(null!);
        
        act.Should().Throw<ArgumentNullException>();
    }
}
```

### Test Isolation Patterns

**Use temporary directories:**
```csharp
public class FileOperationTests : IDisposable
{
    private readonly string _tempDir;
    
    public FileOperationTests()
    {
        _tempDir = Path.Combine(Path.GetTempPath(), $"test_{Guid.NewGuid()}");
        Directory.CreateDirectory(_tempDir);
    }
    
    public void Dispose()
    {
        if (Directory.Exists(_tempDir))
            Directory.Delete(_tempDir, recursive: true);
    }
    
    [Fact]
    public void WriteFile_ValidPath_CreatesFile()
    {
        var filePath = Path.Combine(_tempDir, "test.txt");
        
        File.WriteAllText(filePath, "content");
        
        File.ReadAllText(filePath).Should().Be("content");
    }
}
```

**Use mocking with Moq:**
```csharp
using Moq;

[Fact]
public void Service_FetchData_CallsRepository()
{
    var mockRepo = new Mock<IRepository>();
    mockRepo.Setup(r => r.GetById(It.IsAny<int>()))
            .Returns(new Entity { Id = 1, Name = "Test" });
    
    var service = new Service(mockRepo.Object);
    
    var result = service.GetItem(1);
    
    result.Name.Should().Be("Test");
    mockRepo.Verify(r => r.GetById(1), Times.Once);
}
```

## Package Management: NuGet/dotnet CLI

### Commands
```bash
# Restore packages
dotnet restore

# Add package
dotnet add package PackageName

# Add package with version
dotnet add package PackageName --version 1.0.0

# Build
dotnet build

# Run tests
dotnet test

# Run project
dotnet run

# Publish
dotnet publish -c Release
```

### Project File (.csproj)
```xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>
  
  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
  </ItemGroup>
  
  <ItemGroup Condition="'$(Configuration)' == 'Debug'">
    <PackageReference Include="xunit" Version="2.7.0" />
    <PackageReference Include="Moq" Version="4.20.0" />
    <PackageReference Include="FluentAssertions" Version="6.12.0" />
  </ItemGroup>
</Project>
```

## Code Quality: Analyzers

### Recommended Analyzers
```xml
<ItemGroup>
  <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0">
    <PrivateAssets>all</PrivateAssets>
  </PackageReference>
  <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
    <PrivateAssets>all</PrivateAssets>
  </PackageReference>
</ItemGroup>
```

### EditorConfig (.editorconfig)
```ini
[*.cs]
# Indentation
indent_style = space
indent_size = 4

# Naming conventions
dotnet_naming_rule.private_fields_with_underscore.symbols = private_fields
dotnet_naming_rule.private_fields_with_underscore.style = prefix_underscore
dotnet_naming_rule.private_fields_with_underscore.severity = suggestion

dotnet_naming_symbols.private_fields.applicable_kinds = field
dotnet_naming_symbols.private_fields.applicable_accessibilities = private
dotnet_naming_style.prefix_underscore.capitalization = camel_case
dotnet_naming_style.prefix_underscore.required_prefix = _

# Code style
csharp_prefer_braces = true:warning
csharp_style_var_for_built_in_types = false:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
```

### Commands
```bash
# Format code
dotnet format

# Analyze without building
dotnet build /p:EnforceCodeStyleInBuild=true
```

## Build System Commands

### Full Validation Cycle
```bash
# Clean
dotnet clean

# Restore
dotnet restore

# Build
dotnet build --no-restore -warnaserror

# Test with coverage
dotnet test --no-build --collect:"XPlat Code Coverage"

# Format check
dotnet format --verify-no-changes
```

## Common Patterns

### Logging
```csharp
using Microsoft.Extensions.Logging;

public class Service
{
    private readonly ILogger<Service> _logger;
    
    public Service(ILogger<Service> logger)
    {
        _logger = logger;
    }
    
    public void Process()
    {
        _logger.LogInformation("Processing started");
        _logger.LogDebug("Detailed information");
        _logger.LogWarning("Something unusual");
        _logger.LogError("Something failed");
    }
}
```

### Dependency Injection
```csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IRepository, SqlRepository>();
builder.Services.AddTransient<IService, Service>();
builder.Services.AddSingleton<ICache, MemoryCache>();

var app = builder.Build();
```

### Async Patterns
```csharp
public async Task<Result> ProcessAsync(CancellationToken cancellationToken = default)
{
    // Async with cancellation support
    var data = await _repository.GetDataAsync(cancellationToken);
    
    // Parallel execution
    var tasks = items.Select(item => ProcessItemAsync(item, cancellationToken));
    var results = await Task.WhenAll(tasks);
    
    return new Result(results);
}

// Async enumerable
public async IAsyncEnumerable<Item> StreamItemsAsync(
    [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
    await foreach (var item in _source.ReadAllAsync(cancellationToken))
    {
        yield return item;
    }
}
```

### Error Handling
```csharp
public class AppException : Exception
{
    public string Code { get; }
    public int StatusCode { get; }
    
    public AppException(string message, string code, int statusCode = 500)
        : base(message)
    {
        Code = code;
        StatusCode = statusCode;
    }
}

// Result pattern
public record Result<T>
{
    public T? Value { get; init; }
    public string? Error { get; init; }
    public bool IsSuccess => Error is null;
    
    public static Result<T> Success(T value) => new() { Value = value };
    public static Result<T> Failure(string error) => new() { Error = error };
}
```

### CLI with System.CommandLine
```csharp
using System.CommandLine;

var rootCommand = new RootCommand("CLI description");

var inputOption = new Option<string>(
    name: "--input",
    description: "Input file path");

var verboseOption = new Option<bool>(
    name: "--verbose",
    description: "Enable verbose output");

rootCommand.AddOption(inputOption);
rootCommand.AddOption(verboseOption);

rootCommand.SetHandler((input, verbose) =>
{
    if (verbose)
        Console.WriteLine($"Processing: {input}");
    // Process input
}, inputOption, verboseOption);

await rootCommand.InvokeAsync(args);
```

## Build Pipeline

### Pipeline Stages

A comprehensive .NET build pipeline runs these stages:

| Stage | Tool | Purpose |
|-------|------|---------|
| 1. Restore | `dotnet restore` | Restore NuGet packages |
| 2. Format | `dotnet format` | Auto-format code |
| 3. Build | `dotnet build` | Compile solution |
| 4. Test | `dotnet test` | Run tests with coverage |
| 5. Coverage | ReportGenerator | Generate HTML reports |
| 6. Validate | Coverage threshold | Enforce minimum coverage |

### Build Script Pattern (Batch)

```batch
@echo off
setlocal enabledelayedexpansion

echo ========================================
echo  .NET Build Pipeline
echo ========================================

:: Configuration
set "SOLUTION=MyProject.sln"
set "MIN_COVERAGE=70"
set "ARTIFACTS=artifacts"

:: Stage 1: Restore
echo [1/5] Restoring packages...
dotnet restore %SOLUTION%
if errorlevel 1 goto :error

:: Stage 2: Format
echo [2/5] Formatting code...
dotnet format %SOLUTION% --verify-no-changes
if errorlevel 1 (
    echo WARNING: Code formatting issues found
    dotnet format %SOLUTION%
)

:: Stage 3: Build
echo [3/5] Building solution...
dotnet build %SOLUTION% -c Release --no-restore -warnaserror
if errorlevel 1 goto :error

:: Stage 4: Test with Coverage
echo [4/5] Running tests with coverage...
dotnet test %SOLUTION% -c Release --no-build ^
    --collect:"XPlat Code Coverage" ^
    --results-directory:%ARTIFACTS%\coverage\raw
if errorlevel 1 goto :error

:: Stage 5: Generate Coverage Report
echo [5/5] Generating coverage report...
dotnet tool install --global dotnet-reportgenerator-globaltool 2>nul
reportgenerator ^
    -reports:%ARTIFACTS%\coverage\raw\**\coverage.cobertura.xml ^
    -targetdir:%ARTIFACTS%\coverage\report ^
    -reporttypes:Html;TextSummary

echo ========================================
echo  BUILD SUCCESSFUL
echo ========================================
echo Coverage report: %ARTIFACTS%\coverage\report\index.html
exit /b 0

:error
echo ========================================
echo  BUILD FAILED
echo ========================================
exit /b 1
```

### Coverage Configuration

```xml
<!-- Directory.Build.props - Apply to all projects -->
<Project>
  <PropertyGroup>
    <CollectCoverage>true</CollectCoverage>
    <CoverletOutputFormat>cobertura</CoverletOutputFormat>
    <CoverletOutput>./coverage/</CoverletOutput>
    <ExcludeByAttribute>GeneratedCodeAttribute,ObsoleteAttribute</ExcludeByAttribute>
    <ExcludeByFile>**/Migrations/**</ExcludeByFile>
  </PropertyGroup>
</Project>
```

```xml
<!-- Test project .csproj -->
<ItemGroup>
  <PackageReference Include="coverlet.collector" Version="6.0.0">
    <PrivateAssets>all</PrivateAssets>
    <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
  </PackageReference>
  <PackageReference Include="coverlet.msbuild" Version="6.0.0">
    <PrivateAssets>all</PrivateAssets>
  </PackageReference>
</ItemGroup>
```

### ReportGenerator Configuration

```bash
# Install globally
dotnet tool install --global dotnet-reportgenerator-globaltool

# Generate HTML report
reportgenerator \
    -reports:coverage/**/coverage.cobertura.xml \
    -targetdir:coverage/report \
    -reporttypes:Html;TextSummary;Cobertura

# Generate badges
reportgenerator \
    -reports:coverage/**/coverage.cobertura.xml \
    -targetdir:coverage/badges \
    -reporttypes:Badges
```

### Solution Filter (.slnf) for Faster Builds

```json
// MyProject.Build.slnf
{
  "solution": {
    "path": "MyProject.sln",
    "projects": [
      "src\\MyProject.Core\\MyProject.Core.csproj",
      "src\\MyProject.Api\\MyProject.Api.csproj",
      "tests\\MyProject.Tests\\MyProject.Tests.csproj"
    ]
  }
}
```

```bash
# Build only projects in filter
dotnet build MyProject.Build.slnf
```

### Slow Test Detection (PowerShell)

```powershell
# analyze-slow-tests.ps1
param(
    [string]$TestOutputFile = "test-output.txt",
    [int]$ThresholdMs = 5000
)

$slowTests = @()
$pattern = '(\d+\.?\d*)\s*[ms]+\s*(.+)'

Get-Content $TestOutputFile | ForEach-Object {
    if ($_ -match $pattern) {
        $duration = [double]$matches[1]
        $testName = $matches[2]
        
        if ($duration -gt $ThresholdMs) {
            $slowTests += [PSCustomObject]@{
                Duration = $duration
                Test = $testName
            }
        }
    }
}

if ($slowTests.Count -gt 0) {
    Write-Host "WARNING: Slow tests detected (> ${ThresholdMs}ms):" -ForegroundColor Yellow
    $slowTests | Sort-Object Duration -Descending | Format-Table -AutoSize
}
```

## CI/CD Integration

### GitHub Actions

```yaml
# .github/workflows/dotnet.yml
name: .NET Build

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        dotnet-version: ['8.0.x']
    
    steps:
    - uses: actions/checkout@v4
    
    - name: Setup .NET
      uses: actions/setup-dotnet@v4
      with:
        dotnet-version: ${{ matrix.dotnet-version }}
    
    - name: Restore dependencies
      run: dotnet restore
    
    - name: Check formatting
      run: dotnet format --verify-no-changes
    
    - name: Build
      run: dotnet build --no-restore -warnaserror
    
    - name: Test with coverage
      run: |
        dotnet test --no-build --verbosity normal \
          --collect:"XPlat Code Coverage" \
          --results-directory ./coverage
    
    - name: Generate coverage report
      run: |
        dotnet tool install --global dotnet-reportgenerator-globaltool
        reportgenerator \
          -reports:coverage/**/coverage.cobertura.xml \
          -targetdir:coverage/report \
          -reporttypes:Cobertura
    
    - name: Upload coverage
      uses: codecov/codecov-action@v4
      with:
        files: ./coverage/report/Cobertura.xml
        fail_ci_if_error: true
    
    - name: Upload coverage report
      uses: actions/upload-artifact@v4
      with:
        name: coverage-report
        path: coverage/report/
```

### Azure DevOps

```yaml
# azure-pipelines.yml
trigger:
  - main

pool:
  vmImage: 'ubuntu-latest'

variables:
  buildConfiguration: 'Release'
  DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true

steps:
- task: UseDotNet@2
  displayName: 'Use .NET 8.0'
  inputs:
    version: '8.0.x'

- script: dotnet restore
  displayName: 'Restore packages'

- script: dotnet format --verify-no-changes
  displayName: 'Check formatting'

- script: dotnet build --configuration $(buildConfiguration) --no-restore -warnaserror
  displayName: 'Build'

- task: DotNetCoreCLI@2
  displayName: 'Run tests with coverage'
  inputs:
    command: test
    arguments: '--configuration $(buildConfiguration) --no-build --collect:"XPlat Code Coverage"'
    publishTestResults: true

- script: |
    dotnet tool install --global dotnet-reportgenerator-globaltool
    reportgenerator \
      -reports:$(Agent.TempDirectory)/**/coverage.cobertura.xml \
      -targetdir:$(Build.ArtifactStagingDirectory)/coverage \
      -reporttypes:HtmlInline_AzurePipelines;Cobertura
  displayName: 'Generate coverage report'

- task: PublishCodeCoverageResults@2
  displayName: 'Publish coverage'
  inputs:
    codeCoverageTool: 'Cobertura'
    summaryFileLocation: '$(Build.ArtifactStagingDirectory)/coverage/Cobertura.xml'
    reportDirectory: '$(Build.ArtifactStagingDirectory)/coverage'
```

## Troubleshooting

### Common Issues

#### "dotnet format fails"

```bash
# Install the format tool if missing
dotnet tool install --global dotnet-format

# Run with specific verbosity
dotnet format --verbosity diagnostic

# Fix issues automatically
dotnet format

# Check specific project
dotnet format MyProject.csproj
```

#### "Coverage not collected"

```bash
# Ensure coverlet.collector is installed in test project
dotnet add package coverlet.collector

# Run with explicit collection
dotnet test --collect:"XPlat Code Coverage" --settings runsettings.xml

# Check coverage output location
ls -la TestResults/*/coverage.cobertura.xml
```

#### "ReportGenerator not found"

```bash
# Install as global tool
dotnet tool install --global dotnet-reportgenerator-globaltool

# Or as local tool
dotnet new tool-manifest
dotnet tool install dotnet-reportgenerator-globaltool

# Verify installation
dotnet tool list --global
```

#### "Tests timeout"

```xml
<!-- Create runsettings.xml -->
<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
  <RunConfiguration>
    <TestSessionTimeout>300000</TestSessionTimeout>
  </RunConfiguration>
</RunSettings>
```

```bash
dotnet test --settings runsettings.xml
```

#### "Build warnings treated as errors"

```xml
<!-- Temporarily disable in .csproj -->
<PropertyGroup>
  <TreatWarningsAsErrors>false</TreatWarningsAsErrors>
  <!-- Or suppress specific warnings -->
  <NoWarn>CS1591;CS8618</NoWarn>
</PropertyGroup>
```

#### "Solution filter not working"

```bash
# Verify filter file syntax (JSON)
cat MyProject.Build.slnf | jq .

# Ensure paths are relative to .slnf location
# Use forward slashes in paths
```

#### "Coverlet conflicts with other coverage tools"

```xml
<!-- In test project, use only coverlet.collector -->
<ItemGroup>
  <PackageReference Include="coverlet.collector" Version="6.0.0">
    <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
    <PrivateAssets>all</PrivateAssets>
  </PackageReference>
  <!-- Remove coverlet.msbuild if using collector -->
</ItemGroup>
```
