This guide covers the development workflow, coding standards, and best practices for contributing to PokManagerApi.
- Development Workflow
- TDD with TinyBDD
- Coding Standards
- Testing Guidelines
- Build System
- IDE Setup
- Debugging Tips
- Git Workflow
- Code Review
PokManagerApi follows a strict Test-Driven Development (TDD) approach using TinyBDD:
1. RED → Write a failing test
2. GREEN → Write minimal code to pass
3. REFACTOR → Improve code while keeping tests green
4. REPEAT → Continue for next requirement
-
Understand the Requirement
- Review feature specification
- Identify affected layers (Domain, Application, Infrastructure)
- Plan test scenarios
-
Write Tests First
- Start with TinyBDD tests describing behavior
- Cover happy path and edge cases
- Include error scenarios
-
Implement Minimal Code
- Write just enough code to pass tests
- Don't over-engineer
- Keep it simple
-
Refactor
- Clean up code
- Extract methods/classes
- Improve naming
- Ensure tests still pass
-
Integration
- Test integration with other layers
- Update documentation
- Prepare for code review
Feature: Add "Restart All Instances" functionality
// tests/PokManager.Application.Tests/UseCases/Instances/RestartAllInstancesHandlerTests.cs
public class When_restarting_all_instances : Feature
{
private IRestartAllInstancesHandler _handler;
private IPokManagerClient _mockClient;
private List<string> _instances;
public When_restarting_all_instances()
{
Given.A_handler_with_mocked_dependencies();
And.Multiple_instances_exist();
}
[Fact]
public void Should_restart_all_instances_successfully()
{
When.Restarting_all_instances();
Then.All_instances_should_be_restarted();
And.Operation_should_succeed();
}
[Fact]
public void Should_continue_if_one_instance_fails()
{
Given.One_instance_will_fail_to_restart();
When.Restarting_all_instances();
Then.Other_instances_should_still_be_restarted();
And.Partial_failure_should_be_reported();
}
// Test setup methods using TinyBDD
private void Given.A_handler_with_mocked_dependencies()
{
_mockClient = Substitute.For<IPokManagerClient>();
_handler = new RestartAllInstancesHandler(_mockClient);
}
// Additional setup and assertion methods...
}// src/Core/PokManager.Application/UseCases/Instances/RestartAllInstances/RestartAllInstancesRequest.cs
public record RestartAllInstancesRequest
{
public bool WaitForCompletion { get; init; } = true;
public TimeSpan Timeout { get; init; } = TimeSpan.FromMinutes(5);
}// src/Core/PokManager.Application/UseCases/Instances/RestartAllInstances/RestartAllInstancesResponse.cs
public record RestartAllInstancesResponse
{
public int TotalInstances { get; init; }
public int SuccessfulRestarts { get; init; }
public int FailedRestarts { get; init; }
public IReadOnlyList<string> FailedInstanceIds { get; init; } = Array.Empty<string>();
}// src/Core/PokManager.Application/UseCases/Instances/RestartAllInstances/RestartAllInstancesHandler.cs
public class RestartAllInstancesHandler : IRestartAllInstancesHandler
{
private readonly IPokManagerClient _client;
private readonly ILogger<RestartAllInstancesHandler> _logger;
public RestartAllInstancesHandler(
IPokManagerClient client,
ILogger<RestartAllInstancesHandler> logger)
{
_client = client;
_logger = logger;
}
public async Task<Result<RestartAllInstancesResponse>> HandleAsync(
RestartAllInstancesRequest request,
CancellationToken cancellationToken = default)
{
// Get all instances
var instancesResult = await _client.ListInstancesAsync(cancellationToken);
if (!instancesResult.IsSuccess)
return Result.Failure<RestartAllInstancesResponse>(instancesResult.Error);
var instances = instancesResult.Value;
var failedInstances = new List<string>();
// Restart each instance
foreach (var instanceId in instances)
{
var result = await _client.RestartInstanceAsync(instanceId, cancellationToken);
if (!result.IsSuccess)
{
_logger.LogWarning("Failed to restart instance {InstanceId}: {Error}",
instanceId, result.Error);
failedInstances.Add(instanceId);
}
}
// Build response
var response = new RestartAllInstancesResponse
{
TotalInstances = instances.Count,
SuccessfulRestarts = instances.Count - failedInstances.Count,
FailedRestarts = failedInstances.Count,
FailedInstanceIds = failedInstances
};
return Result.Success(response);
}
}# Run tests
dotnet test tests/PokManager.Application.Tests/
# All tests should pass
# Refactor as needed while keeping tests greenTinyBDD provides a behavior-driven testing approach with fluent syntax.
public class When_performing_action : Feature // Feature base class from TinyBDD
{
// Arrange - Test setup
private readonly MyClass _sut;
private readonly IMockDependency _mockDep;
public When_performing_action()
{
// Constructor = "Given" setup
_mockDep = Substitute.For<IMockDependency>();
_sut = new MyClass(_mockDep);
}
// Act + Assert
[Fact]
public void Should_produce_expected_result()
{
// Arrange (additional)
var input = "test";
// Act
var result = _sut.DoSomething(input);
// Assert
result.Should().Be("expected");
}
}public class When_starting_an_instance : Feature
{
[Fact]
public void Should_succeed_when_instance_is_stopped()
{
// Given
Given.An_instance_exists();
And.Instance_is_stopped();
// When
When.Starting_the_instance();
// Then
Then.Operation_should_succeed();
And.Instance_should_be_running();
}
// Helper methods
private void Given.An_instance_exists() { /* setup */ }
private void And.Instance_is_stopped() { /* setup */ }
private void When.Starting_the_instance() { /* action */ }
private void Then.Operation_should_succeed() { /* assertion */ }
private void And.Instance_should_be_running() { /* assertion */ }
}Test class names follow the pattern: When_[action]_[context]
Test method names follow: Should_[expected_result]_[condition]
Examples:
When_starting_an_instance/Should_succeed_when_instance_is_stoppedWhen_parsing_status_output/Should_parse_running_instance_correctlyWhen_creating_a_backup/Should_fail_when_instance_not_found
Use FluentAssertions for readable assertions:
// ✅ Good - Fluent and readable
result.IsSuccess.Should().BeTrue();
result.Value.Should().NotBeNull();
result.Value.InstanceId.Should().Be("server1");
failedInstances.Should().BeEmpty();
// ❌ Bad - Less readable
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
Assert.Equal("server1", result.Value.InstanceId);
Assert.Empty(failedInstances);// Create mock
var mockClient = Substitute.For<IPokManagerClient>();
// Setup return value
mockClient.StartInstanceAsync("server1")
.Returns(Result.Success(Unit.Default));
// Verify call was made
await mockClient.Received(1).StartInstanceAsync("server1");
// Verify call was NOT made
await mockClient.DidNotReceive().StopInstanceAsync(Arg.Any<string>());// Classes, interfaces, methods - PascalCase
public class InstanceManager { }
public interface IPokManagerClient { }
public async Task StartInstanceAsync() { }
// Private fields - _camelCase with underscore
private readonly IPokManagerClient _client;
private string _instanceId;
// Local variables, parameters - camelCase
public void Process(string instanceId)
{
var result = DoSomething();
}
// Constants - PascalCase
public const int MaxRetryAttempts = 3;
// Properties - PascalCase
public string InstanceId { get; set; }// 1. Usings
using System;
using PokManager.Domain;
// 2. Namespace
namespace PokManager.Application.UseCases;
// 3. Class/Interface
public class StartInstanceHandler
{
// 4. Constants
private const int MaxRetries = 3;
// 5. Fields
private readonly IPokManagerClient _client;
// 6. Constructor
public StartInstanceHandler(IPokManagerClient client)
{
_client = client;
}
// 7. Public methods
public async Task<Result<Unit>> HandleAsync() { }
// 8. Private methods
private void ValidateRequest() { }
}// ✅ Good - Simple property
public bool IsRunning => State == InstanceState.Running;
// ✅ Good - Simple method
public string GetDisplayName() => $"{Name} ({Id})";
// ❌ Bad - Too complex for expression body
public bool IsValid() => !string.IsNullOrEmpty(Id) && State != InstanceState.Unknown && CreatedAt < DateTime.UtcNow;// ✅ Good - Pattern matching
return status switch
{
InstanceState.Running => "The instance is running",
InstanceState.Stopped => "The instance is stopped",
InstanceState.Failed => "The instance has failed",
_ => "Unknown status"
};
// ❌ Bad - If-else chain
if (status == InstanceState.Running)
return "The instance is running";
else if (status == InstanceState.Stopped)
return "The instance is stopped";
// ...// ✅ Good - Immutable record
public record StartInstanceRequest(string InstanceId, bool WaitForStart = true);
// ❌ Bad - Mutable class for DTO
public class StartInstanceRequest
{
public string InstanceId { get; set; }
public bool WaitForStart { get; set; }
}// Enabled in .csproj
<Nullable>enable</Nullable>
// ✅ Good - Explicit nullability
public string? ErrorMessage { get; set; } // Can be null
public string InstanceId { get; set; } // Never null
// Use null-forgiving operator sparingly
var value = nullableValue!; // Only when you're certainAlways use Result<T> for operations that can fail:
// ✅ Good - Result<T>
public Result<InstanceStatus> GetStatus(string instanceId)
{
if (string.IsNullOrEmpty(instanceId))
return Result.Failure<InstanceStatus>("Instance ID is required");
var status = FetchStatus(instanceId);
return status != null
? Result.Success(status)
: Result.Failure<InstanceStatus>("Instance not found");
}
// ❌ Bad - Throwing exceptions for flow control
public InstanceStatus GetStatus(string instanceId)
{
if (string.IsNullOrEmpty(instanceId))
throw new ArgumentException("Instance ID is required");
var status = FetchStatus(instanceId);
if (status == null)
throw new InstanceNotFoundException();
return status;
}Add XML documentation for public APIs:
/// <summary>
/// Starts the specified ARK server instance.
/// </summary>
/// <param name="instanceId">The unique identifier of the instance to start.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A result indicating success or failure of the start operation.</returns>
/// <exception cref="ArgumentNullException">Thrown when instanceId is null.</exception>
public async Task<Result<Unit>> StartInstanceAsync(
string instanceId,
CancellationToken cancellationToken = default)
{
// Implementation
}- Domain Layer: 100% coverage (business logic must be fully tested)
- Application Layer: 95%+ coverage (all use cases and validation)
- Infrastructure Layer: 90%+ coverage (complex logic fully tested)
- Presentation Layer: 80%+ coverage (key components and workflows)
tests/
├── PokManager.Domain.Tests/
│ ├── Common/
│ │ └── ResultTests.cs # Result<T> tests
│ ├── Entities/
│ │ └── InstanceTests.cs # Instance entity tests
│ └── ValueObjects/
│ └── InstanceIdTests.cs # Value object tests
│
├── PokManager.Application.Tests/
│ ├── UseCases/
│ │ ├── Instances/
│ │ │ ├── StartInstanceHandlerTests.cs
│ │ │ └── StopInstanceHandlerTests.cs
│ │ └── Backups/
│ │ └── CreateBackupHandlerTests.cs
│ └── Validation/
│ └── StartInstanceValidatorTests.cs
│
├── PokManager.Infrastructure.Tests/
│ ├── PokManager/
│ │ ├── Parsers/
│ │ │ ├── StatusOutputParserTests.cs
│ │ │ └── BackupListParserTests.cs
│ │ └── PokManagerClientTests.cs
│ └── Docker/
│ └── DockerCommandBuilderTests.cs
│
└── PokManager.Web.Tests/
└── Components/
└── InstanceCardTests.cs
Use categories to organize test runs:
[Trait("Category", "Unit")]
public class When_parsing_status_output : Feature { }
[Trait("Category", "Integration")]
public class When_executing_real_commands : Feature { }
[Trait("Category", "E2E")]
public class When_user_starts_instance_via_ui : Feature { }Run specific categories:
# Unit tests only
dotnet test --filter "Category=Unit"
# Integration tests
dotnet test --filter "Category=Integration"Use builders for complex test data:
public class InstanceStatusBuilder
{
private string _instanceId = "default-server";
private InstanceState _state = InstanceState.Running;
private ProcessHealth _health = ProcessHealth.Healthy;
public InstanceStatusBuilder WithInstanceId(string id)
{
_instanceId = id;
return this;
}
public InstanceStatusBuilder WithState(InstanceState state)
{
_state = state;
return this;
}
public InstanceStatus Build()
{
return new InstanceStatus
{
InstanceId = _instanceId,
State = _state,
Health = _health
};
}
}
// Usage in tests
var status = new InstanceStatusBuilder()
.WithInstanceId("test-server")
.WithState(InstanceState.Stopped)
.Build();# Full build
dotnet build
# Build specific configuration
dotnet build --configuration Release
# Build specific project
dotnet build src/Core/PokManager.Domain/PokManager.Domain.csproj
# Parallel build (faster)
dotnet build -m
# Verbose output
dotnet build --verbosity detailed# Clean all projects
dotnet clean
# Clean and rebuild
dotnet clean && dotnet buildPokManagerApi will use Nuke for build automation:
# Install Nuke global tool
dotnet tool install Nuke.GlobalTool --global
# Run default build
nuke
# Run specific target
nuke Test
# Run clean build
nuke Clean Build Test- ASP.NET and web development
- .NET desktop development
- .NET Aspire SDK
- ReSharper or CodeMaid (code cleanup)
- Visual Studio Spell Checker
- Markdown Editor
- GitLens (if not using built-in Git)
-
Tools > Options > Text Editor > C# > Code Style > General
- Set naming conventions
- Enable EditorConfig support
-
Tools > Options > Text Editor > C# > Advanced
- Enable "Place 'System' directives first when sorting usings"
-
Test Explorer
- Group by: Project, Namespace, Class
- TinyBDD Test Runner (if available)
- .NET Aspire Support
- Heap Allocations Viewer
-
Settings > Editor > Code Style > C#
- Import code style from
.editorconfig
- Import code style from
-
Settings > Build, Execution, Deployment > Unit Testing
- Enable continuous testing (optional)
- C# Dev Kit (Microsoft)
- C# (Microsoft)
- .NET Aspire
- GitLens
- Thunder Client (API testing)
- Markdown All in One
- Better Comments
{
"dotnet.defaultSolution": "PokManager.sln",
"omnisharp.enableRoslynAnalyzers": true,
"omnisharp.enableEditorConfigSupport": true,
"csharp.format.enable": true
}- Set breakpoint in test
- Right-click test in Test Explorer
- Select "Debug"
- Set breakpoint in test
- Click debug icon next to test
- Or use Ctrl+Shift+D
- Set breakpoint in test
- Open Test Explorer
- Right-click test > Debug Test
# Run with debugger attached
dotnet run --project src/Hosting/PokManager.AppHost --launch-profile https
# View logs in Aspire Dashboard
# Navigate to https://localhost:15000
# Click on service to view logsAdd detailed logging:
_logger.LogDebug("Executing command: {Command}", command);
_logger.LogDebug("Command output: {Output}", output);View logs in:
- Console output
- Aspire Dashboard
- Log files (if configured)
// Add detailed assertion messages
result.IsSuccess.Should().BeTrue(
"because the instance should start successfully when it's stopped");
// Log intermediate values
_testOutputHelper.WriteLine($"Result: {result}");
_testOutputHelper.WriteLine($"Error: {result.Error}");// Log command before execution
_logger.LogInformation("Executing: {Command}", command);
// Log raw output
_logger.LogDebug("Raw output: {Output}", rawOutput);
_logger.LogDebug("Raw error: {Error}", rawError);feature/short-description- New featuresbugfix/issue-description- Bug fixesrefactor/what-changed- Refactoringdocs/what-documented- Documentationtest/what-tested- Test improvements
Examples:
feature/restart-all-instancesbugfix/parser-null-referencerefactor/command-buildersdocs/architecture-diagrams
Follow Conventional Commits:
type(scope): subject
body (optional)
footer (optional)
Types:
feat: New featurefix: Bug fixdocs: Documentationtest: Testsrefactor: Code refactoringchore: Maintenance
Examples:
feat(application): add restart all instances use case
Implements a new use case to restart all server instances
in parallel with failure handling.
Resolves #123
---
fix(parsers): handle null output in status parser
The status parser was throwing NullReferenceException
when given null input. Now returns Result.Failure.
---
test(domain): add Result<T> edge case tests
Adds tests for Result<T> chaining and error propagation.
Before committing:
- ✅ All tests pass (
dotnet test) - ✅ Code builds without warnings (
dotnet build) - ✅ Code follows style guidelines
- ✅ New code has tests
- ✅ Documentation updated (if needed)
- ✅ No commented-out code
- ✅ No debug statements (Console.WriteLine, etc.)
-
Create a feature branch
git checkout -b feature/my-feature
-
Write tests and implementation
- Follow TDD workflow
- Ensure all tests pass
-
Update documentation
- Update README if needed
- Add/update XML docs
- Update architecture docs if applicable
-
Commit changes
git add . git commit -m "feat(scope): description"
-
Push and create PR
git push origin feature/my-feature
## Description
Brief description of changes
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
## Testing
- [ ] All existing tests pass
- [ ] New tests added
- [ ] Manual testing completed
## Checklist
- [ ] Code follows style guidelines
- [ ] Self-review completed
- [ ] Documentation updated
- [ ] No new warnings
## Related Issues
Closes #123- ✅ Follows Clean Architecture principles
- ✅ Correct layer placement
- ✅ Dependencies point inward
- ✅ Tests written before implementation (TDD)
- ✅ Tests are comprehensive
- ✅ Tests are readable and maintainable
- ✅ Edge cases covered
- ✅ Code is readable and maintainable
- ✅ Naming is clear and consistent
- ✅ No code duplication
- ✅ Proper error handling (Result)
- ✅ XML documentation for public APIs
- ✅ No obvious performance issues
- ✅ Async/await used correctly
- ✅ No unnecessary allocations
Happy coding! Remember: Tests first, then implementation!