| name | develop |
| description | This skill should be used for significant development work including new projects, major features, refactoring, architecture changes, and migrations. Use when the task involves multiple files/services, external integrations, schema changes, deployment infrastructure, or architectural decisions requiring documentation. Do not use for trivial changes like typo fixes, simple renames, minor bug fixes within existing patterns, documentation-only updates, or configuration value changes. |
Develop Skill
Complete development methodology for architecting and engineering significant development work following established patterns and conventions.
Decision-Making Philosophy
Autonomous with Rationale
Make decisions confidently when:
- Requirements are clear
- Established patterns apply
- Technology choice is obvious based on complexity
- Best practice is well-known
Ask questions when:
- Requirements are ambiguous
- Multiple valid approaches exist with different tradeoffs
- User preferences unclear
- Business constraints unknown
Always document:
- Architectural decisions in ADRs (when
adr/directory exists) - Rationale for technology choices
- Tradeoffs considered
- Why alternative approaches were rejected
Technology Stack Decision Framework
Default: Prefer C# for:
- Multiple delivery methods (file, blob, service bus, webhook)
- Schema management and validation
- Azure SDK integration
- Complex error handling
- Asynchronous operations
- Strong typing requirements
Fallback: Bash only for:
- Simple, single-purpose scripts
- Minimal logic (< 100 lines)
- No external API integrations
- Shell operations (file manipulation, system commands)
- Quick automation tasks
Document in ADR-001:
**Decision:** [C# with .NET 10 | Bash script]
**Rationale:** [Why this choice - analyze complexity factors]
**Tradeoffs:** [What was gained vs what was given up]
**Date:** [YYYY-MM-DD]
Code Style Guide
C# Conventions
Classes & Structure
// Default: internal sealed classes
internal sealed class ServiceName
{
// Implementation
}
// Public only when part of API contract
public class PublicApiModel
{
// Implementation
}
Primary constructors for simple DI classes:
internal sealed class ReportGenerator(
ILogger<ReportGenerator> _log,
DataService _dataService,
ConfigService _config)
{
// Fields automatically created from parameters
public async Task<Report> GenerateAsync()
{
_log.LogInformation("Generating report...");
return await _dataService.FetchAsync().ConfigureAwait(false);
}
}
Traditional constructors for complex classes:
internal sealed class ComplexService
{
/* Dependencies */
private readonly DataService _dataService;
private readonly ConfigService _config;
private readonly ILogger<ComplexService> _log;
/// <summary>
/// Default injectable constructor.
/// </summary>
public ComplexService(
DataService dataService,
ConfigService config,
ILogger<ComplexService> log)
{
_dataService = dataService;
_config = config;
_log = log;
}
// Methods
}
File organization:
- One class per file
- File name matches class name exactly (e.g.,
ReportGenerator.cs) - PascalCase for all file names
Properties & Fields
// Init-only properties with collection expressions
public string Name { get; init; } = string.Empty;
public List<string> Tags { get; init; } = [];
public Dictionary<string, object> Metadata { get; init; } = [];
// Nullable properties - explicit annotation
public string? OptionalValue { get; init; }
public DateTime? ExpiryDate { get; init; }
// Private fields - underscore prefix, camelCase
private readonly ILogger<ServiceName> _log;
private readonly ConfigService _config;
// Get-only properties for immutable values
public string TenantId { get; }
Nullable reference types:
// Always enable in .csproj
<Nullable>enable</Nullable>
// Explicit null handling
if (value is null)
{
throw new ArgumentNullException(nameof(value));
}
string? result = GetValue();
if (result is not null)
{
ProcessValue(result);
}
// Null coalescing
var final = value ?? defaultValue;
Methods
// Async methods - always Async suffix
public async Task<Result> ProcessAsync()
{
return await _service.FetchAsync().ConfigureAwait(false);
}
// ConfigureAwait(false) - always use
await SomeAsyncMethod().ConfigureAwait(false);
// Expression-bodied for simple one-liners
public string GetFullName() => $"{FirstName} {LastName}";
public override string ToString() => _value.ToLowerInvariant();
// Full methods for complex logic
public async Task<Report> GenerateReportAsync()
{
_log.LogInformation("Starting report generation...");
var data = await FetchDataAsync().ConfigureAwait(false);
var processed = ProcessData(data);
var report = CreateReport(processed);
_log.LogInformation("Report generation complete");
return report;
}
// Private static helpers
private static bool ValidateInput(string input)
{
return !string.IsNullOrWhiteSpace(input) && input.Length <= 100;
}
Data Models
Records for DTOs and request models:
/// <summary>
/// Request model for creating a user.
/// </summary>
internal record CreateUserRequest(
string FirstName,
string LastName,
string Email,
string? PhoneNumber
);
// Records with init properties
public sealed record UserDetails
{
[JsonPropertyName("id")]
public string Id { get; init; } = string.Empty;
[JsonPropertyName("displayName")]
public string DisplayName { get; init; } = string.Empty;
[JsonPropertyName("email")]
public string Email { get; init; } = string.Empty;
[JsonPropertyName("tags")]
public List<string> Tags { get; init; } = [];
}
Classes for domain models with complex constructors:
internal class UserEntity
{
public string Id { get; init; } = string.Empty;
public string Name { get; init; } = string.Empty;
public List<string> Roles { get; init; } = [];
// Default constructor
public UserEntity() { }
// Constructor from external model
public UserEntity(ExternalUser externalUser)
{
Id = externalUser.Identifier ?? string.Empty;
Name = $"{externalUser.FirstName} {externalUser.LastName}";
Roles = externalUser.Permissions?.Select(p => p.Name).ToList() ?? [];
}
}
Classes for services with stateful behavior:
internal sealed class CacheService
{
private readonly Dictionary<string, CachedValue> _cache = [];
private readonly ILogger<CacheService> _log;
public CacheService(ILogger<CacheService> log)
{
_log = log;
}
public void Set(string key, object value)
{
_cache[key] = new CachedValue(value, DateTime.UtcNow);
_log.LogDebug("Cached value for key: {Key}", key);
}
}
LINQ & Collections
// Fluent chaining with method syntax
var results = items
.Where(x => !string.IsNullOrEmpty(x.Name))
.Select(x => new { x.Name, x.Value })
.OrderBy(x => x.Name)
.ThenByDescending(x => x.Value)
.ToList();
// Custom extension methods (Map, AddWhen patterns)
var mapped = items
.Map(x => Transform(x))
.AddWhen(condition, additionalItem);
// Lambda formatting
.Select(x => x.Name) // Single line
.Where(x => !string.IsNullOrEmpty(x.Value)) // Single line
.Select(item => new ComplexModel // Multi-line when complex
{
Id = item.Id,
Name = item.Name,
Processed = ProcessValue(item.Value)
})
// Collection expressions (C# 12)
List<string> items = [];
Dictionary<string, int> map = [];
string[] array = [];
Constants Organization
/// <summary>
/// Application constants organized by context.
/// </summary>
public static class Constants
{
public static class ResourceTypes
{
public const string WebApp = "WebApp";
public const string FunctionApp = "FunctionApp";
public const string Database = "Database";
public static readonly List<string> All = [
WebApp,
FunctionApp,
Database
];
}
public static class Defaults
{
public const int TimeoutSeconds = 30;
public const int RetryCount = 3;
public const string DefaultRegion = "eastus";
}
}
// Usage
var type = Constants.ResourceTypes.WebApp;
foreach (var resourceType in Constants.ResourceTypes.All)
{
// Process each type
}
Logging Patterns
// Structured logging - always use placeholders, never string interpolation
_log.LogInformation(
"Processing request... UserId={UserId};RequestType={RequestType}",
userId,
requestType);
_log.LogError(ex,
"Failed to process request: {Error}",
ex.Message);
_log.LogWarning(
"Threshold exceeded: {CurrentValue} > {MaxValue}",
currentValue,
maxValue);
// Logger naming
private readonly ILogger<ServiceName> _log; // Always named _log
Documentation
XML docs on public APIs:
/// <summary>
/// Generates a report based on provided criteria.
/// </summary>
/// <param name="criteria">The filtering and sorting criteria.</param>
/// <returns>A complete report with all requested data.</returns>
/// <exception cref="ArgumentNullException">When criteria is null.</exception>
/// <exception cref="ValidationException">When criteria is invalid.</exception>
public async Task<Report> GenerateReportAsync(ReportCriteria criteria)
{
// Implementation
}
Code organization with regions:
// Public members - no wrapping needed, keep visible
public void PublicMethod() { }
#region Internals
private void HelperMethod() { }
private void AnotherHelper() { }
#endregion
Purpose:
- Use
#region Internals/#endregionto wrap ONLY private/internal members - Public API stays unwrapped and fully visible
- Regions allow collapsing implementation details in IDE
- Section comment lines (
// ----) only for Bash/scripting languages
Inline comments - only when necessary:
// Only when business logic needs clarification
if (flags.Count(x => x) > 1)
{
// Business rule: Cannot filter by multiple statuses simultaneously
throw new InvalidOperationException("Cannot filter by more than one status");
}
// Self-documenting code preferred over comments
var activeUsers = users.Where(u => u.IsActive && u.LastLogin > cutoffDate);
Modern C# Features
// Records for immutable data
public sealed record Configuration(
string ApiUrl,
int TimeoutSeconds,
bool EnableRetry
);
// Pattern matching
if (value is null) return defaultValue;
if (content is string stringContent) await ProcessStringAsync(stringContent);
var result = response switch
{
{ StatusCode: 200 } => "Success",
{ StatusCode: 404 } => "Not Found",
_ => "Error"
};
// Init-only properties
public string Name { get; init; } = string.Empty;
// Target-typed new
Dictionary<string, string> headers = new() { ["Authorization"] = token };
List<int> numbers = new() { 1, 2, 3 };
// Collection expressions (C# 12)
List<string> items = [];
string[] names = [name1, name2, name3];
// Top-level statements (Program.cs)
var host = CreateHost();
await host.RunAsync();
// Primary constructors (C# 12)
internal sealed class Service(ILogger<Service> _log, Config _config)
{
public void Process() => _log.LogInformation("Processing...");
}
Bash Conventions
#!/bin/bash
#######################################################################
# Constants
#######################################################################
# Color codes for output
_error="\e[0;31m##[error]" # Red
_warning="\e[0;33m##[warning]" # Yellow
_command="\e[0;34m##[command]" # Blue
_debug="\e[0;35m##[debug]" # Purple
_0="\e[0m" # Reset
#######################################################################
# Functions
#######################################################################
function print_banner() {
echo "########################################################"
echo "# Tool Name - Brief Description"
echo "#"
echo "# Additional context"
echo "#"
echo "# Author: Joshua Sprague"
echo "########################################################"
}
function check_env() {
if [[ -z "$REQUIRED_VAR" ]]; then
echo -e "${_error}Missing required environment variable REQUIRED_VAR"
exit 1
fi
}
function print_debug_info() {
echo
echo -e "${_debug}SECRET_VALUE=${SECRET_VALUE:0:3}*****"
echo -e "${_debug}PUBLIC_VALUE=$PUBLIC_VALUE"
}
function login_to_azure() {
echo -e "${_command}az login -p *** -u *** --tenant ***"
az login -p "$AZURE_SECRET" -u $AZURE_CLIENT_ID --tenant $AZURE_TENANT_ID > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo -e "${_error}Azure login failed"
exit 1
fi
}
#######################################################################
# Main Script
#######################################################################
print_banner
check_env
print_debug_info
login_to_azure
# Business logic here
echo -e "${_0}\nDone - Operation completed successfully"
Key patterns:
- Color-coded output for different message types
- Functions organized logically (not alphabetically)
- Check exit codes after critical operations (
$?) - Mask secrets in logs (
${SECRET:0:3}*****) - Environment variable configuration
- Banner for tool identification
- Validation function (
check_env)
Project Structure Patterns
Console Applications
ProjectName.Console/
├── Domain/
│ ├── [Feature]/ # Feature-based folders (Employee, Project, etc.) - optional
│ │ ├── [Model].cs # Domain models (optional)
│ │ └── [Service].cs # Business logic services
│ ├── Models/ # Domain models (optional - only if mapping from entities)
│ ├── Services/ # Domain services (if not feature-organized)
│ └── CommandBuilders/ # CLI command handlers
├── Infrastructure/
│ ├── Entities/ # Database entities (EF Core, CosmosDB, etc.) - if applicable
│ ├── Messages/ # Integration message DTOs - if applicable
│ │ ├── Incoming/ # Service Bus, Event Grid inbound
│ │ └── Outgoing/ # Service Bus, Event Grid outbound
│ ├── Services/ # Infrastructure services
│ │ ├── [Entity]Repo.cs # Data access repositories
│ │ ├── Integration/ # External API clients
│ │ └── Utilities/ # Infrastructure utilities
│ └── [DbContext].cs # EF Core DbContext - if applicable
├── Outputs/ # Delivery mechanisms (file, blob, etc.)
├── Resources/ # Embedded resources
├── Migrations/ # EF Core migrations - if applicable
├── Constants.cs
├── GlobalUsings.cs
├── Program.cs
└── AppConfig.cs
# Test project
Tests.ProjectName.Console/
├── Domain/
│ └── Services/
│ └── ServiceNameTests.cs
├── Infrastructure/
│ └── Services/
│ └── RepoTests.cs
└── TestFixtures/
Azure Functions
ProjectName.Functions/
├── Api/ # HTTP-triggered functions
│ ├── [Feature]/ # Feature-based organization
│ │ ├── GetItems.cs
│ │ ├── CreateItem.cs
│ │ └── UpdateItem.cs
│ └── [OtherFeature]/
│ └── ProcessEvent.cs
├── Functions/ # Non-HTTP triggers (Timer, ServiceBus, Cosmos, etc.)
│ ├── Timer_[Name].cs
│ ├── ServiceBus_[Name].cs
│ └── CosmosDb_[Name].cs
├── Domain/
│ ├── [Feature]/ # Feature folders (Employee, Project, etc.) - optional
│ │ ├── [Model].cs # Domain models (optional)
│ │ └── [Service].cs # Business logic
│ ├── Models/ # Domain models (optional - only if mapping from entities)
│ └── Services/ # Domain services (if not feature-organized)
├── Infrastructure/
│ ├── Entities/ # Database entities (EF Core, CosmosDB) - if applicable
│ ├── Messages/ # Integration message DTOs - if applicable
│ │ ├── Incoming/ # Service Bus, Event Grid inbound
│ │ └── Outgoing/ # Service Bus, Event Grid outbound
│ ├── Realtime/ # SignalR hubs - if applicable
│ ├── Services/ # Infrastructure services
│ │ ├── [Entity]Repo.cs # Data access repositories
│ │ ├── Integration/ # External API clients
│ │ └── Utilities/ # Infrastructure utilities
│ └── [DbContext].cs # EF Core DbContext - if applicable
├── Migrations/ # EF Core migrations - if applicable
├── _FunctionBootstrap.cs # Shared bootstrap
├── Constants.cs
├── GlobalUsings.cs
└── Program.cs
# Test project
Tests.ProjectName.Functions/
├── Api/
├── Domain/
└── Infrastructure/
Containerized Tools
project-name/
├── .github/
│ └── workflows/
│ └── ci-cd.yaml
├── src/
│ ├── Domain/
│ │ ├── [Feature]/ # Feature folders - optional
│ │ ├── Models/ # Domain models (optional - only if mapping from entities)
│ │ └── Services/ # Business logic services
│ ├── Infrastructure/
│ │ ├── Entities/ # Database entities - if applicable
│ │ ├── Messages/ # Integration DTOs - if applicable
│ │ │ ├── Incoming/
│ │ │ └── Outgoing/
│ │ ├── Services/ # Infrastructure services
│ │ │ ├── [Entity]Repo.cs
│ │ │ ├── Integration/
│ │ │ └── Utilities/
│ │ └── [DbContext].cs # DbContext - if applicable
│ ├── Outputs/ # Delivery mechanisms
│ ├── Migrations/ # EF Core migrations - if applicable
│ ├── Constants.cs
│ ├── GlobalUsings.cs
│ ├── Program.cs
│ └── ProjectName.csproj
├── tests/
│ └── ProjectName.Tests/
│ ├── Domain/
│ └── Infrastructure/
├── Dockerfile
├── docker-compose.yml
├── makefile
├── README.md
├── CHANGELOG.md
└── .gitignore
Blazor Web Applications
ProjectName.Web/
├── Components/ # Blazor UI layer
│ ├── Layout/
│ ├── Pages/
│ │ ├── [Feature]/
│ │ └── [OtherFeature]/
│ └── Shared/
│ └── [Feature]/
├── Domain/
│ ├── [Feature]/ # Feature folders (Employee, Project, Security, etc.)
│ │ ├── [Model].cs # Domain models (optional)
│ │ └── [Service].cs # Business logic
│ ├── Models/ # Domain models (optional - only if mapping from entities)
│ └── Services/ # Domain services (if not feature-organized)
├── Infrastructure/
│ ├── Entities/ # Database entities (EF Core, CosmosDB)
│ ├── Messages/ # Integration message DTOs
│ │ ├── Incoming/ # Service Bus, Event Grid inbound
│ │ └── Outgoing/ # Service Bus, Event Grid outbound
│ ├── Realtime/ # SignalR hubs
│ │ ├── [Hub].cs
│ │ └── ConnectionCache.cs
│ ├── Services/ # Infrastructure services
│ │ ├── [Entity]Repo.cs # Data access repositories
│ │ ├── Integration/ # External API clients
│ │ └── Utilities/ # Infrastructure utilities
│ └── AppDbContext.cs # EF Core DbContext
├── Migrations/ # EF Core migrations
├── wwwroot/ # Static web assets
├── Constants.cs
├── GlobalUsings.cs
├── Program.cs
└── ProjectName.Web.csproj
# Test project
Tests.ProjectName.Web/
├── Components/
├── Domain/
└── Infrastructure/
Libraries / NuGet Packages
ProjectName/
├── src/
│ └── ProjectName/
│ ├── Models/
│ ├── Services/
│ ├── Interfaces/
│ └── ProjectName.csproj
├── tests/
│ └── ProjectName.Tests/
├── README.md
├── CHANGELOG.md
└── LICENSE
Naming Conventions
Projects:
Company.Product.Component(e.g.,dci.ErmApi.Console)toolset--service--purpose(e.g.,aztools--kubernetes--cluster-shutdown)- Consistent pattern within organization
Namespaces:
- Match directory structure
Company.Product.Component.Layer(e.g.,dci.ErmApi.Console.Domain.Services)
Files:
- PascalCase, match class name exactly
- One class per file
Layer Separation: Infrastructure vs Domain
Infrastructure Layer - Data access, external integrations, infrastructure concerns
Contains:
- Entities/ - ALL database entities
- EF Core entity classes
- CosmosDB/DocumentDB documents
- Database schema mappings
- Navigation properties for relationships
- Messages/ - Integration message DTOs
- Incoming/ - Service Bus, Event Grid inbound messages
- Outgoing/ - Service Bus, Event Grid outbound messages
- External system message formats (lowercase properties matching external format)
- Realtime/ - SignalR hubs and real-time infrastructure (if applicable)
- Hub classes
- Connection management
- Real-time message models
- Services/ - Infrastructure services
- [Entity]Repo.cs - Data access repositories (thin, just DbContext access)
- Integration/ - External API HTTP clients
- Utilities/ - Infrastructure utility classes
- Infrastructure concerns: caching, authentication, message processors
- [DbContext].cs - EF Core database context
Does NOT contain:
- Business logic
- Domain models (unless they ARE the entities)
- UI components
Domain Layer - Business logic and domain models
Contains:
- [Feature]/ - Feature-based organization (Employee/, Project/, Security/) - optional
- Models/ - Rich domain models (optional - only if mapping from entities)
- Business logic via extension methods
- Domain-specific behavior
- NOT database entities
- Services/ - Business logic services
- Orchestration between repos and domain logic
- Business rules and workflows
- Can depend on Infrastructure services
- Value Objects - Domain value objects (enums, structs)
- Business Rules - Authorization logic, permission checks
Does NOT contain:
- Database entities (those belong in Infrastructure/Entities)
- Data access code (those belong in Infrastructure repos)
- External API clients (those belong in Infrastructure/Services/Integration)
- Integration DTOs (those belong in Infrastructure/Messages)
Migrations/ - EF Core migrations (if applicable)
Location: Project root (sibling to Domain and Infrastructure)
Contains:
- EF Core migration files
- Model snapshot
Dependency Direction: Domain → Infrastructure (Pragmatic, not Clean Architecture)
- Domain services CAN depend on Infrastructure services
- Domain models CAN use Infrastructure value objects
- Infrastructure does NOT depend on Domain
- This is simplified, pragmatic architecture for developer velocity
Example:
using ProjectName.Infrastructure; // ✅ Allowed
using ProjectName.Infrastructure.Services; // ✅ Allowed
namespace ProjectName.Domain.Employee;
internal class EmployeeService(
CacheService cache, // Infrastructure service
EmployeeRepo repo, // Infrastructure repo
ExternalApiService api) // Infrastructure integration
{
// Business logic here - orchestrates Infrastructure services
}
When to use Domain/Models/:
- Only when mapping from Infrastructure entities to rich domain models
- Domain models have business logic, behavior, extension methods
- Not always necessary - many apps work directly with entities
Example mapping:
Infrastructure/Entities/User.cs (EF Core entity - simple data class)
↓ (mapped to)
Domain/Models/UserDetails.cs (Rich model with business logic methods)
Architecture Decision Records (ADRs)
When to Create adr/ Directory
Create adr/ directory when project involves:
- Evaluating multiple technology options (C# vs Python vs Go)
- Significant architectural patterns (microservices, CQRS, event sourcing)
- Complex deployment tradeoffs (Kubernetes vs App Service vs Functions)
- Performance/scalability architecture decisions
- Security architecture requiring justification
- Database technology choices (SQL vs NoSQL vs hybrid)
- Any decision with long-term implications requiring documentation
Skip adr/ directory when project:
- Follows established patterns without deviation
- Has obvious technology choice (no alternatives evaluated)
- Is a simple tool/script (< 500 lines)
- Has no significant architectural tradeoffs
- Is straightforward implementation of known patterns
ADR Format
File naming: adr/[number]-[short-title].md
Examples:
adr/0001-record-architecture-decisions.mdadr/0002-use-csharp-for-backend.mdadr/0003-deploy-with-container-app-jobs.md
Template:
# [Number]. [Title]
Date: YYYY-MM-DD
## Status
[Proposed | Accepted | Deprecated | Superseded by ADR-XXXX]
## Context
What is the issue we're seeing that is motivating this decision or change?
## Decision
What is the change that we're proposing and/or doing?
## Consequences
What becomes easier or more difficult to do because of this change?
Example ADR-0001
# 1. Record Architecture Decisions
Date: 2025-01-15
## Status
Accepted
## Context
We need to record the architectural decisions made on this project so that
future team members can understand why certain choices were made.
## Decision
We will use Architecture Decision Records, as described by Michael Nygard in
his article: http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions
We will keep ADRs in the `adr/` directory in the project repository.
## Consequences
- Architectural decisions will be documented and version controlled
- Team members can understand context behind decisions
- ADRs become part of project documentation
Example ADR-0002 (Technology Stack)
# 2. Use C# with .NET 10 for Backend Implementation
Date: 2025-01-15
## Status
Accepted
## Context
This project requires multiple output delivery methods (file, blob storage,
service bus, webhook), complex schema validation, Azure SDK integration, and
robust error handling. We need to choose a technology that supports these
requirements while maintaining type safety and developer productivity.
## Decision
We will use C# with .NET 10 for the backend implementation.
Alternatives considered:
- **Bash scripting**: Simple deployment but too complex for error handling and
JSON manipulation at this scale
- **Python**: Good Azure SDK support but lacks strong typing without mypy,
less familiar to team
- **Go**: Fast and self-contained but less mature Azure SDK, team less experienced
## Consequences
**Positive:**
- Strong typing catches errors at compile time
- Native async/await patterns for concurrent operations
- Mature Azure SDK with full feature support
- Better IDE tooling and refactoring support
- Team has strong C# experience
**Negative:**
- Larger container image compared to Go
- Slightly longer cold start compared to compiled languages
- Requires .NET runtime in container
When to Create Each ADR
Always create if present:
- ADR-0001: Record architecture decisions (documents decision to use ADRs)
Create when applicable:
- ADR-000X: Technology stack choice (if evaluated alternatives)
- ADR-000X: Deployment model (if multiple options considered)
- ADR-000X: Database choice (if SQL vs NoSQL decision made)
- ADR-000X: Authentication strategy (if multiple approaches evaluated)
- ADR-000X: API design patterns (if REST vs GraphQL vs gRPC decision)
- ADR-000X: State management (if complex client state decisions)
Location:
adr/directory at project root- Committed to git repository
- Referenced in README.md
Testing
Testing Requirements
Tests are mandatory for:
- All business logic in services
- Data transformations and validation logic
- Integration with external systems
- Complex algorithms and workflows
Testing Implementation
Use the /qa-automation-expert skill for all testing tasks:
- Test infrastructure setup
- Test generation and implementation
- Test analysis and quality assessment
- Follow InfiniteLoop.Testing.Xunit patterns
Basic test project structure:
ProjectName.Tests/
├── Infrastructure/
│ ├── TestBase.cs
│ ├── Startup.cs
│ └── xunit.runner.json
├── Domain/
│ └── Services/
│ └── The{ServiceName}.cs
├── Outputs/
│ └── The{OutputName}.cs
└── ScenarioTests/
└── {Workflow}ScenarioTests.cs
Essential requirements:
- Follow InfiniteLoop.Testing.Xunit patterns (
The{ClassName}naming, required attributes) - Use Shouldly for assertions
- Given-When-Then structure with block comments
- All tests must pass before considering work complete
For comprehensive testing implementation, use: /qa-automation-expert
Versioning Strategies
Conventional Commits
Format:
<type>: all lowercase, single sentence describing change
Types:
chore:- build/tooling changes, dependency updatesfix:- bug fixes (patch version bump)feat:- new features (minor version bump)docs:- documentation only changes
Rules:
- ✅ Always lowercase
- ✅ Always single sentence
- ✅ No period at end
- ✅ Descriptive but concise (50 chars or less preferred)
Examples:
✅ feat: add blob storage output delivery
✅ fix: handle null certificate thumbprints gracefully
✅ chore: update azure sdk packages to latest versions
✅ docs: add installation instructions for windows
❌ feat: Add blob storage (capitalized)
❌ fix: Handle null certs. Also update logging. (multiple sentences)
❌ chore: Updated packages (past tense, capitalized)
❌ Feat: add feature (wrong format)
Console Applications (with Auto-Update)
.csproj version configuration:
<PropertyGroup>
<Version Condition="'$(Version)' == ''">0.0.0</Version>
<AssemblyVersion>$(Version)</AssemblyVersion>
<FileVersion>$(Version)</FileVersion>
<InformationalVersion Condition="'$(InformationalVersion)' == ''">$(Version)-local</InformationalVersion>
</PropertyGroup>
Version retrieval at runtime:
public static string GetVersion()
{
try
{
var assembly = Assembly.GetEntryAssembly();
var version = assembly?.GetName().Version;
if (version != null)
{
return $"v{version.Major}.{version.Minor}.{version.Build}";
}
}
catch
{
// Fall through
}
return "unknown";
}
VersionCheckService pattern:
internal sealed class VersionCheckService
{
private const int CacheTtlMinutes = 5;
private readonly string _cacheFilePath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".appname",
".version-check");
public async Task<VersionCheckResult?> CheckForUpdatesAsync(bool forceRefresh = false)
{
var cache = await ReadCacheAsync();
if (cache != null && !cache.IsExpired() && !forceRefresh)
{
// Return cached result, fire-and-forget background refresh
_ = Task.Run(async () => await RefreshCacheAsync());
return new VersionCheckResult(
cache.CurrentVersion,
cache.LatestVersion,
cache.UpdateAvailable);
}
// Force refresh or cache expired
return await RefreshCacheAsync();
}
}
GitHub Actions release workflow:
on:
push:
tags:
- 'v*.*.*'
jobs:
release:
strategy:
matrix:
include:
- rid: osx-arm64
os: macOS
- rid: linux-x64
os: Linux
- rid: win-x64
os: Windows
steps:
- name: Extract version
id: version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
- name: Build
run: |
dotnet publish \
--runtime ${{ matrix.rid }} \
--self-contained true \
/p:PublishSingleFile=true \
/p:Version=${{ steps.version.outputs.VERSION }}
Containerized Tools
Dockerfile version support:
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
ARG VERSION=local
WORKDIR /src
COPY ["ProjectName.csproj", "."]
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish \
/p:Version=${VERSION}
FROM mcr.microsoft.com/dotnet/runtime:8.0-alpine
ARG VERSION=local
ENV TOOL_VERSION=${VERSION}
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "ProjectName.dll"]
Build with version:
docker build --build-arg VERSION=1.2.3 -t myimage:1.2.3 .
Version in tool output:
var version = Environment.GetEnvironmentVariable("TOOL_VERSION") ?? "local";
Console.WriteLine($"Tool Version: {version}");
// Or embed in report/output
var report = new Report
{
ToolVersion = GetVersion(),
GeneratedAt = DateTime.UtcNow
};
Delivery / Output Patterns
File Output (Always Available)
internal sealed class FileOutput
{
private readonly string _outputDirectory;
private readonly string _fileName;
private readonly ILogger<FileOutput> _log;
public FileOutput(AppConfig config, ILogger<FileOutput> log)
{
_outputDirectory = config.OutputDirectory ?? "/output";
_fileName = config.OutputFileName ?? "output.json";
_log = log;
}
public async Task WriteAsync<T>(T data)
{
Directory.CreateDirectory(_outputDirectory);
var filePath = Path.Combine(_outputDirectory, _fileName);
var json = JsonSerializer.Serialize(data, new JsonSerializerOptions
{
WriteIndented = true
});
await File.WriteAllTextAsync(filePath, json).ConfigureAwait(false);
_log.LogInformation("Wrote output to {FilePath}", filePath);
}
}
Configuration:
OUTPUT_DIRECTORY- Default:/outputOUTPUT_FILENAME- Default:output.jsonoroutput.csv
Azure Blob Storage (Optional)
internal sealed class BlobStorageOutput
{
private readonly BlobServiceClient _blobClient;
private readonly string _containerName;
private readonly string _blobName;
private readonly ILogger<BlobStorageOutput> _log;
public BlobStorageOutput(AppConfig config, ILogger<BlobStorageOutput> log)
{
var connectionString = config.StorageConnectionString;
_blobClient = new BlobServiceClient(connectionString);
_containerName = config.StorageContainerName!;
_blobName = config.StorageBlobName ?? $"output-{DateTime.UtcNow:yyyyMMddHHmmss}.json";
_log = log;
}
public async Task UploadAsync<T>(T data)
{
var container = _blobClient.GetBlobContainerClient(_containerName);
await container.CreateIfNotExistsAsync().ConfigureAwait(false);
var blob = container.GetBlobClient(_blobName);
var json = JsonSerializer.Serialize(data);
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json));
await blob.UploadAsync(stream, overwrite: true).ConfigureAwait(false);
_log.LogInformation("Uploaded to blob: {BlobName}", _blobName);
}
}
Configuration:
STORAGE_CONNECTION_STRINGorSTORAGE_ACCOUNT_NAME(with managed identity)STORAGE_CONTAINER_NAMESTORAGE_BLOB_NAME(optional, generates timestamp-based name)
Azure Service Bus (Optional)
internal sealed class ServiceBusOutput
{
private readonly ServiceBusClient _client;
private readonly string _queueOrTopicName;
private readonly ILogger<ServiceBusOutput> _log;
public ServiceBusOutput(AppConfig config, ILogger<ServiceBusOutput> log)
{
var connectionString = config.ServiceBusConnectionString;
_client = new ServiceBusClient(connectionString);
_queueOrTopicName = config.ServiceBusQueueOrTopicName!;
_log = log;
}
public async Task SendAsync<T>(T data)
{
var sender = _client.CreateSender(_queueOrTopicName);
var json = JsonSerializer.Serialize(data);
var message = new ServiceBusMessage(json);
await sender.SendMessageAsync(message).ConfigureAwait(false);
_log.LogInformation("Sent message to {Destination}", _queueOrTopicName);
await sender.CloseAsync().ConfigureAwait(false);
}
}
Configuration:
SERVICE_BUS_CONNECTION_STRINGorSERVICE_BUS_NAMESPACE(with managed identity)SERVICE_BUS_QUEUE_NAMEorSERVICE_BUS_TOPIC_NAME
Webhook / HTTP POST (Optional)
internal sealed class WebhookOutput
{
private readonly string _webhookUrl;
private readonly string? _authHeader;
private readonly HttpClient _httpClient;
private readonly ILogger<WebhookOutput> _log;
public WebhookOutput(AppConfig config, HttpClient httpClient, ILogger<WebhookOutput> log)
{
_webhookUrl = config.WebhookUrl!;
_authHeader = config.WebhookAuthHeader;
_httpClient = httpClient;
_log = log;
}
public async Task PostAsync<T>(T data)
{
var json = JsonSerializer.Serialize(data);
var content = new StringContent(json, Encoding.UTF8, "application/json");
if (!string.IsNullOrEmpty(_authHeader))
{
_httpClient.DefaultRequestHeaders.Authorization =
AuthenticationHeaderValue.Parse(_authHeader);
}
var response = await _httpClient.PostAsync(_webhookUrl, content).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
_log.LogInformation("Posted to webhook: {Url}", _webhookUrl);
}
}
Configuration:
WEBHOOK_URLWEBHOOK_AUTH_HEADER(optional, e.g.,Bearer token)
Multi-Delivery Orchestration
internal sealed class DeliveryService
{
private readonly FileOutput _fileOutput;
private readonly BlobStorageOutput? _blobOutput;
private readonly ServiceBusOutput? _serviceBusOutput;
private readonly WebhookOutput? _webhookOutput;
private readonly ILogger<DeliveryService> _log;
public async Task DeliverAsync<T>(T data)
{
var deliveryTasks = new List<Task>();
// Always write to file
deliveryTasks.Add(_fileOutput.WriteAsync(data));
// Optional deliveries
if (_blobOutput != null)
deliveryTasks.Add(_blobOutput.UploadAsync(data));
if (_serviceBusOutput != null)
deliveryTasks.Add(_serviceBusOutput.SendAsync(data));
if (_webhookOutput != null)
deliveryTasks.Add(_webhookOutput.PostAsync(data));
// Execute all deliveries concurrently
try
{
await Task.WhenAll(deliveryTasks).ConfigureAwait(false);
_log.LogInformation("All deliveries completed successfully");
}
catch (Exception ex)
{
_log.LogError(ex, "One or more deliveries failed: {Error}", ex.Message);
throw;
}
}
}
Documentation Standards
Architecture Decision Records (ADRs)
Purpose: Committed architectural documentation for significant decisions
Location: adr/ directory in project root (committed to git)
Format: Cognitect ADR format (Status, Context, Decision, Consequences)
When to create:
- Evaluating multiple technology options
- Significant architectural patterns/decisions
- Complex deployment tradeoffs
- Database or framework choices
- Any decision with long-term implications
When to skip:
- Straightforward implementations
- Obvious technology choices
- Simple tools (< 500 lines)
See ADR Framework section for full details and examples.
README.md
Purpose: User-facing project documentation
Target audience: Competent IT professionals Style: Clear, concise, no pandering, brevity over verbosity
Required sections:
# Project Name
Brief description (1-2 sentences about what this tool/application does)
## Features
- Bullet list of key capabilities
- Each feature on one line
- Focus on user-facing functionality
## Requirements
- Brief list of prerequisites
- No excessive explanation
- Example: "Service principal with Reader role on target subscriptions"
## Configuration
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `VAR_NAME` | Yes | - | Brief description |
| `OPTIONAL_VAR` | No | value | Brief description |
## Usage
\`\`\`bash
# Copy-paste ready examples
docker run --rm \\
-e VAR1=value \\
-e VAR2=value \\
-v $(pwd)/output:/output \\
image:tag
\`\`\`
\`\`\`bash
# Concise deployment command
az containerapp job create \\
--name job-name \\
--resource-group rg \\
--image image:tag \\
--env-vars VAR1=value VAR2=value
\`\`\`
## Infrastructure Setup
\`\`\`bash
az ad sp create-for-rbac \\
--name "sp-name" \\
--role Reader \\
--scopes /subscriptions/SUB_ID
\`\`\`
\`\`\`bash
az storage account create \\
--name storageacct \\
--resource-group rg
\`\`\`
## Development
\`\`\`bash
# Build
dotnet build
# Test
dotnet test
# Run locally
dotnet run
\`\`\`
## Versioning
Uses conventional commits and semantic versioning.
Commit format: `<type>: lowercase, single sentence`
Types: `chore`, `fix`, `feat`, `docs`
Inline Documentation
XML docs on public APIs:
/// <summary>
/// Brief description of what this class/method does.
/// </summary>
/// <param name="paramName">What this parameter represents.</param>
/// <returns>What is returned.</returns>
/// <exception cref="ExceptionType">When this exception is thrown.</exception>
Code organization:
// Public members - no wrapping
public void PublicMethod() { }
#region Internals
private void HelperMethod() { }
#endregion
Inline comments - only when necessary:
- Business rule clarifications
- Non-obvious algorithm explanations
- Workarounds for external API quirks
Workflow: 9 Phases
Phase 0: Significance Detection & Analysis
Analyze request scope and complexity:
Assess project requirements
- Count files/classes/services to be created/modified
- Identify integrations (external APIs, databases, queues)
- Assess schema/data model requirements
- Check for deployment/infrastructure changes
Gather requirements
- Ask clarifying questions if requirements are ambiguous
- Understand full scope of work
- Identify technical and business constraints
- Determine if ADR documentation will be needed
Phase 1: Planning & Architecture
Create architectural foundation:
Assess whether to create
adr/directoryCreate
adr/directory if:- Evaluating multiple technology options
- Significant architectural patterns/decisions
- Complex deployment tradeoffs
- Performance/scalability architecture
- Security architecture requiring justification
- Database technology choices
- Any decision with long-term implications
Skip
adr/directory if:- Straightforward implementation following established patterns
- Obvious technology choice
- Simple tool/script (< 500 lines)
- No significant architectural tradeoffs
Make technology stack decision (autonomous)
- Analyze complexity factors (delivery methods, schema, integrations)
- Choose C# or Bash based on framework
- Document decision in
adr/0001-technology-stack.mdifadr/directory created
Create todo list
- Break implementation into phases
- Track documentation tasks
- Track implementation progress
Design schema (if applicable)
- Create strongly-typed records/classes
- Document in ADR if significant data modeling decisions
Plan integrations
- Identify authentication approach
- Plan delivery methods
- Document in ADRs if complex integration architecture
Phase 2: Project Setup
Establish project structure:
Create directory structure based on project needs
- Always create:
Domain/ - Create
Infrastructure/if:- Using a database (EF Core, CosmosDB)
- Calling external APIs
- Using messaging (Service Bus, Event Grid)
- Using SignalR
- Create subdirectories as needed:
Domain/[Feature]/for feature-based organization (optional)Domain/Models/only if mapping from entities to domain modelsDomain/Services/for business logic servicesInfrastructure/Entities/for database entitiesInfrastructure/Messages/Incoming/and/Outgoing/for integration DTOsInfrastructure/Realtime/for SignalR hubsInfrastructure/Services/for repos and infrastructure servicesInfrastructure/Services/Integration/for external API clientsInfrastructure/Services/Utilities/for infrastructure utilitiesMigrations/at project root if using EF Core
- For specific project types:
Api/for Azure Functions HTTP triggersFunctions/for Azure Functions non-HTTP triggersComponents/for Blazor UIOutputs/for delivery mechanisms (console/containerized tools)
- Create test project structure mirroring Domain/ and Infrastructure/
- Always create:
Initialize .NET project (if C#)
- Target framework: net8.0 or net10.0
- Enable nullable reference types
- Add required NuGet packages:
Microsoft.EntityFrameworkCore.SqlServer(if using EF Core)Azure.Identity,Azure.ResourceManager.*(if using Azure SDK)Azure.Messaging.ServiceBus(if using Service Bus)Azure.Storage.Blobs(if using Blob Storage)
- Create GlobalUsings.cs
- Create Constants.cs
Initialize Infrastructure layer (if applicable)
- Create DbContext in
Infrastructure/[DbContext].cs - Set up connection string loading
- Configure entity classes in
Infrastructure/Entities/ - Create repository services in
Infrastructure/Services/[Entity]Repo.cs - Set up EF Core migrations with
dotnet ef migrations add Initial
- Create DbContext in
Initialize Domain layer
- Create feature folders or flat Services/ structure
- Create domain services in
Domain/Services/orDomain/[Feature]/ - Create
Domain/Models/only if mapping from entities to rich domain models - Domain services depend on Infrastructure repos/services
Create configuration
- AppConfig class with Keys nested class
- Environment variable loading
- Validation in constructor
Create supporting files
- .gitignore (include .claude/, .vs/, bin/, obj/, *.user, etc.)
- Initial README.md structure
Phase 3: Core Implementation
Build the application:
Implement models
- Records for DTOs
- Classes for domain models
- JSON serialization attributes
- Validation attributes
Implement services
- Business logic with error handling
- Structured logging throughout
- Async/await patterns with ConfigureAwait(false)
- Dependency injection via constructors
Implement delivery/output
- FileOutput (always)
- Optional: BlobStorageOutput, ServiceBusOutput, WebhookOutput
- DeliveryService orchestration
Implement Program.cs
- Banner/header
- DI container setup
- Service registration
- Execution flow
- Error handling
Phase 4: Testing
Ensure quality through comprehensive testing:
Use
/qa-automation-expertskill for testing implementation- Implements InfiniteLoop.Testing.Xunit patterns
- Creates test infrastructure (Startup.cs, xunit.runner.json, base classes)
- Generates comprehensive test coverage
- Ensures all tests follow required patterns
Minimum testing requirements
- Test project mirrors main project structure
- All business logic has unit tests
- Integration tests for external dependencies
- Scenario tests for workflows (if applicable)
- All tests must pass
Verify test quality
- All tests follow InfiniteLoop.Testing.Xunit patterns
- Proper naming (
The{ClassName}) - Required attributes present
- Given-When-Then structure
- Shouldly assertions used
Phase 5: Containerization (if applicable)
Package for deployment:
Create Dockerfile
- Multi-stage build (build → runtime)
- ARG VERSION=local for version injection
- ENV TOOL_VERSION=${VERSION}
- Optimized layer caching
- RUN apk update && apk upgrade
Create docker-compose.yml
- Local testing configuration
- Volume mounts
- Environment variables
- Service definitions
Create makefile
- build target
- publish target
- Version detection (if applicable)
- Clean targets
Test Docker build
- Build image locally
- Run container
- Validate volume mounts
- Test all delivery methods
Phase 6: Versioning (if applicable)
Set up version management:
- Configure .csproj versioning
<Version Condition="'$(Version)' == ''">0.0.0</Version>
<AssemblyVersion>$(Version)</AssemblyVersion>
<FileVersion>$(Version)</FileVersion>
<InformationalVersion>$(Version)-local</InformationalVersion>
Add version retrieval
- Assembly metadata retrieval
- Format as vX.Y.Z
For console apps with auto-update:
- Implement VersionCheckService (5-minute cache)
- Implement version command
- Implement upgrade command
- Create install scripts (bash, PowerShell)
- Create GitHub Actions release workflow (multi-platform)
- SHA256 checksum generation
For containers:
- Version in Docker tags
- Version in tool output/reports
- ENV TOOL_VERSION in Dockerfile
Phase 7: Documentation
Document everything:
Create/update README.md
- All required sections
- Copy-paste ready examples
- Clear, concise instructions
- Infrastructure setup steps
- Configuration table
Create/update CHANGELOG.md
- Initial release entry
- Follow conventional changelog format
Add inline documentation
- XML docs on all public APIs
- Section comments for organization
- Inline comments only when necessary
Phase 8: Validation & Refactoring
Ensure quality and consistency:
Run all tests
- Unit tests pass
- Integration tests pass
- Validate coverage
Test execution
- Local (dotnet run or Docker)
- Validate all features work
- Test error handling
- Test all delivery methods
Review code quality
- Check against style guide
- Verify nullable annotations
- Verify ConfigureAwait(false) usage
- Verify structured logging
Identify refactoring opportunities
- Modern C# features used?
- Primary constructors where applicable?
- Collection expressions instead of new()?
- Nullable reference types enabled?
Suggest refactoring
- Proactively suggest modernization
- Document refactoring tasks in todo list
- Explain benefits
- User can accept or decline
Phase 9: Completion
Wrap up the work:
Mark all todos complete
- Verify every task done
- Clear completed items
Verify deliverables checklist
- Code follows style guide ✅
- Tests implemented and passing ✅
- README.md comprehensive ✅
- Documentation synchronized ✅
- ADRs complete (if applicable) ✅
Provide summary
- What was built
- Key decisions made
- How to use it
- Next steps (if any)
Note follow-up items
- Suggested enhancements
- Known limitations to address
- Additional testing needed
Refactoring Guidelines
Detection
When editing existing code, automatically analyze:
Nullable reference types enabled?
- Check .csproj for
<Nullable>enable</Nullable> - If missing, suggest enabling
- Check .csproj for
Modern C# features used?
- Primary constructors for simple classes?
- Collection expressions (= []) vs old syntax (= new())?
- Init-only properties vs set properties?
- Records for DTOs?
Logging patterns followed?
- Structured logging with placeholders?
- Logger named
_log? - Never string interpolation in log messages?
Async patterns correct?
- ConfigureAwait(false) used?
- Async suffix on method names?
Code organization?
- One class per file?
- Section comments for organization?
- XML docs on public APIs?
Suggestions
Always suggest refactoring when touching code that:
- Doesn't use nullable reference types
- Uses old collection initialization (new List
() vs []) - Doesn't use primary constructors for simple DI classes
- Uses string interpolation in logging
- Missing ConfigureAwait(false)
- Poorly organized (multiple classes per file)
Suggestion format:
I notice this file doesn't use [modern feature].
Current pattern:
[show current code]
Recommended pattern:
[show improved code]
Benefits:
- [benefit 1]
- [benefit 2]
Should I refactor while making these changes? (This is recommended but you can decline)
Document refactoring:
- Add as separate todo items
- Explain benefits in comments/ADRs if significant architectural changes
Deliverables Checklist
Before marking work complete, verify:
Code Quality:
- ✅ Follows C# or Bash style guide
- ✅ Nullable reference types enabled (C#)
- ✅ Modern C# features used appropriately
- ✅ Structured logging throughout
- ✅ ConfigureAwait(false) on all awaits
- ✅ Proper error handling
Project Structure:
- ✅ Matches conventions for project type
- ✅ One class per file
- ✅ Organized with Domain/, Services/, Models/
- ✅ GlobalUsings.cs created
- ✅ Constants.cs organized with nested classes
Testing:
- ✅ Tests implemented using
/qa-automation-expertskill - ✅ All tests passing
- ✅ InfiniteLoop.Testing.Xunit patterns followed
Documentation:
- ✅ ADRs documented (if applicable)
- ✅ README.md comprehensive and clear
- ✅ All required sections present
- ✅ Copy-paste ready examples
- ✅ Infrastructure setup documented
- ✅ CHANGELOG.md maintained
- ✅ XML docs on public APIs
- ✅ Inline comments where necessary
Versioning (if applicable):
- ✅ .csproj version configuration
- ✅ Version retrieval implemented
- ✅ Console apps: version command, upgrade command
- ✅ Containers: VERSION build arg, TOOL_VERSION env var
- ✅ Install scripts (if applicable)
Containerization (if applicable):
- ✅ Dockerfile with multi-stage build
- ✅ docker-compose.yml for local testing
- ✅ makefile with build targets
- ✅ Tested Docker build and execution
CI/CD (if applicable):
- ✅ GitHub Actions workflow
- ✅ Required secrets documented in README
- ✅ Release automation configured
General:
- ✅ .gitignore appropriate for project type (includes .claude/)
- ✅ Todo list completed
- ✅ Refactoring suggestions made
- ✅ All deliverables synchronized