Skip to main content
This guide walks you through setting up a complete development environment for Wolfix.Server and building your first feature.

Prerequisites

Ensure you have completed the Quickstart and have the application running locally.

Development Tools

JetBrains Rider

Best-in-class .NET IDE with excellent refactoring tools

Visual Studio 2022

Full-featured IDE with great debugging capabilities

VS Code

Lightweight editor with C# Dev Kit extension

Visual Studio for Mac

macOS-native .NET development

Required Extensions

  • C# Dev Kit (VS Code)
  • Entity Framework Core Power Tools (Visual Studio)
  • .NET Aspire workload

Database Tools

  • pgAdmin or DBeaver for PostgreSQL
  • MongoDB Compass for MongoDB
  • Postman or Insomnia for API testing

Project Structure

Understanding the solution structure:
Wolfix.Server/
├── Wolfix.Server.sln        # Solution file
├── Wolfix.API/               # Main API project (entry point)
├── Wolfix.AppHost/           # .NET Aspire orchestration
├── Wolfix.ServiceDefaults/   # Shared Aspire configuration
├── Shared.*/                 # Shared components
├── {Module}.Domain/          # Domain layer per module
├── {Module}.Application/     # Application layer per module
├── {Module}.Infrastructure/  # Infrastructure layer per module
├── {Module}.Endpoints/       # Endpoints layer per module
├── {Module}.IntegrationEvents/ # Event contracts per module
└── {Module}.Tests/           # Tests per module

Development Workflow

1. Create a New Branch

git checkout -b feature/my-new-feature

2. Build the Solution

dotnet build

3. Run Tests

# Run all tests
dotnet test

# Run tests for specific module
dotnet test Catalog.Tests/Catalog.Tests.csproj

# Run tests with coverage
dotnet test --collect:"XPlat Code Coverage"

4. Run the Application

cd Wolfix.AppHost
dotnet run
Or use your IDE’s run configuration.

Building a New Feature

Let’s build a feature: Add product rating summary.
1

Add to Domain Layer

Start with the domain - add business logic to the entity.
Catalog.Domain/ProductAggregate/Product.cs
public sealed class Product : BaseEntity
{
    // Existing properties...
    public double? AverageRating { get; private set; }
    public int ReviewCount { get; private set; }
    
    // New method to get rating summary
    public ProductRatingSummary GetRatingSummary()
    {
        if (_reviews.Count == 0)
            return ProductRatingSummary.NoReviews();
        
        var ratingCounts = _reviews
            .GroupBy(r => r.Rating)
            .ToDictionary(g => g.Key, g => g.Count());
        
        return new ProductRatingSummary(
            AverageRating ?? 0,
            ReviewCount,
            ratingCounts
        );
    }
    
    private void RecalculateAverageRating()
    {
        if (_reviews.Count == 0)
        {
            AverageRating = null;
            ReviewCount = 0;
            return;
        }
        
        AverageRating = Math.Round(
            _reviews.Average(r => r.Rating), 
            MidpointRounding.AwayFromZero
        );
        ReviewCount = _reviews.Count;
    }
}
Catalog.Domain/ProductAggregate/ValueObjects/ProductRatingSummary.cs
public record ProductRatingSummary(
    double AverageRating,
    int TotalReviews,
    Dictionary<uint, int> RatingCounts
)
{
    public static ProductRatingSummary NoReviews() => new(
        0,
        0,
        new Dictionary<uint, int>()
    );
};
2

Add to Application Layer

Create DTOs and add service methods.
Catalog.Application/Dto/ProductRatingSummaryDto.cs
public record ProductRatingSummaryDto(
    double AverageRating,
    int TotalReviews,
    Dictionary<uint, int> RatingDistribution
);
Catalog.Application/Services/ProductService.cs
public async Task<Result<ProductRatingSummaryDto>> GetRatingSummaryAsync(
    Guid productId, 
    CancellationToken ct)
{
    Product? product = await _productRepository.GetByIdAsync(productId, ct);
    
    if (product == null)
        return Result<ProductRatingSummaryDto>.Failure(
            "Product not found", 
            HttpStatusCode.NotFound
        );
    
    ProductRatingSummary summary = product.GetRatingSummary();
    
    var dto = new ProductRatingSummaryDto(
        summary.AverageRating,
        summary.TotalReviews,
        summary.RatingCounts
    );
    
    return Result<ProductRatingSummaryDto>.Success(dto);
}
3

Add to Endpoints Layer

Create the HTTP endpoint.
Catalog.Endpoints/Endpoints/ProductEndpoints.cs
public static IEndpointRouteBuilder MapProductEndpoints(this IEndpointRouteBuilder app)
{
    var group = app.MapGroup("/api/products")
        .WithTags("Products");
    
    // Existing endpoints...
    
    group.MapGet("/{id:guid}/rating-summary", GetRatingSummary)
        .WithName("GetProductRatingSummary")
        .Produces<ProductRatingSummaryDto>()
        .Produces(404);
    
    return app;
}

private static async Task<IResult> GetRatingSummary(
    Guid id,
    [FromServices] ProductService productService,
    CancellationToken ct)
{
    var result = await productService.GetRatingSummaryAsync(id, ct);
    
    return result.IsSuccess
        ? Results.Ok(result.Value)
        : Results.Problem(
            detail: result.ErrorMessage,
            statusCode: (int)result.StatusCode
          );
}
4

Write Tests

Add unit tests for domain logic.
Catalog.Tests/Domain/ProductRatingSummaryTests.cs
public class ProductRatingSummaryTests
{
    [Fact]
    public void GetRatingSummary_WithNoReviews_ShouldReturnEmptySummary()
    {
        // Arrange
        var productResult = Product.Create(
            "Test", "Desc", 99m, 
            ProductStatus.Active, 
            Guid.NewGuid(), 
            Guid.NewGuid()
        );
        var product = productResult.Value!;
        
        // Act
        var summary = product.GetRatingSummary();
        
        // Assert
        Assert.Equal(0, summary.AverageRating);
        Assert.Equal(0, summary.TotalReviews);
        Assert.Empty(summary.RatingCounts);
    }
    
    [Fact]
    public void GetRatingSummary_WithMultipleReviews_ShouldCalculateCorrectly()
    {
        // Arrange
        var productResult = Product.Create(
            "Test", "Desc", 99m, 
            ProductStatus.Active, 
            Guid.NewGuid(), 
            Guid.NewGuid()
        );
        var product = productResult.Value!;
        
        product.AddReview("Good", "Nice", 5, Guid.NewGuid());
        product.AddReview("OK", "Decent", 4, Guid.NewGuid());
        product.AddReview("Great", "Love it", 5, Guid.NewGuid());
        
        // Act
        var summary = product.GetRatingSummary();
        
        // Assert
        Assert.Equal(4.7, summary.AverageRating, 1); // 14/3 = 4.67 ≈ 4.7
        Assert.Equal(3, summary.TotalReviews);
        Assert.Equal(2, summary.RatingCounts[5]);
        Assert.Equal(1, summary.RatingCounts[4]);
    }
}
5

Test Manually

Run the application and test with curl or Postman:
curl http://localhost:5000/api/products/{product-id}/rating-summary
Expected response:
{
  "averageRating": 4.5,
  "totalReviews": 10,
  "ratingDistribution": {
    "5": 6,
    "4": 2,
    "3": 1,
    "2": 1
  }
}

Database Migrations

Creating a Migration

When you modify entities, create a migration:
# From solution root
dotnet ef migrations add AddRatingSummaryFields \
  --project Catalog.Infrastructure \
  --startup-project Wolfix.API

Applying Migrations

# Apply to database
dotnet ef database update \
  --project Catalog.Infrastructure \
  --startup-project Wolfix.API

Removing Last Migration

dotnet ef migrations remove \
  --project Catalog.Infrastructure \
  --startup-project Wolfix.API
Never modify migration files manually after they’ve been applied to production. Create a new migration instead.

Debugging

Debug with .NET Aspire

  1. Set breakpoints in your code
  2. Run Wolfix.AppHost in debug mode
  3. Aspire will launch all dependencies
  4. Your breakpoints will be hit

Debug a Specific Module

  1. Set Wolfix.API as startup project
  2. Configure environment variables in launchSettings.json
  3. Run containers manually:
    docker run -d -p 27017:27017 mongo
    docker run -d -p 8000:8000 iluhahr/toxic-ai-api:latest
    
  4. Start debugging

Debug with Hot Reload

.NET 9 supports hot reload for most code changes:
dotnet watch --project Wolfix.API
Changes to most C# files will be applied without restarting.

Code Style

Naming Conventions

  • Classes: PascalCase - ProductService
  • Interfaces: PascalCase with I prefix - IProductRepository
  • Methods: PascalCase - GetProductAsync
  • Parameters: camelCase - productId
  • Private fields: camelCase with _ prefix - _productRepository

Domain Layer Rules

1

Use Factory Methods

// ✅ Good
public static Result<Product> Create(...) { }

// ❌ Bad
public Product(...) { } // Public constructor
2

Private Setters

// ✅ Good
public string Title { get; private set; }

// ❌ Bad
public string Title { get; set; }
3

Encapsulate Collections

// ✅ Good
private readonly List<Review> _reviews = [];
public IReadOnlyCollection<Review> Reviews => _reviews.AsReadOnly();

// ❌ Bad
public List<Review> Reviews { get; set; }
4

Return Result Types

// ✅ Good
public VoidResult ChangePrice(decimal price) { }

// ❌ Bad
public void ChangePrice(decimal price) { throw new Exception(); }

Common Tasks

Add a New Module

1

Create Projects

dotnet new classlib -n MyModule.Domain
dotnet new classlib -n MyModule.Application
dotnet new classlib -n MyModule.Infrastructure
dotnet new classlib -n MyModule.Endpoints
dotnet new classlib -n MyModule.IntegrationEvents
dotnet new xunit -n MyModule.Tests
2

Add Project References

dotnet add MyModule.Application reference MyModule.Domain
dotnet add MyModule.Infrastructure reference MyModule.Application
dotnet add MyModule.Endpoints reference MyModule.Application
# etc.
3

Create DbContext

MyModule.Infrastructure/MyModuleDbContext.cs
public class MyModuleDbContext : DbContext
{
    public DbSet<MyEntity> MyEntities { get; set; }
    
    public MyModuleDbContext(DbContextOptions<MyModuleDbContext> options)
        : base(options) { }
}
4

Register Module

MyModule.Endpoints/Extensions/ServiceCollectionExtensions.cs
public static IServiceCollection AddMyModule(
    this IServiceCollection services,
    string connectionString)
{
    services.AddDbContext<MyModuleDbContext>(options =>
        options.UseNpgsql(connectionString));
    
    // Register repositories, services, etc.
    
    return services;
}
Wolfix.API/Extensions/WebApplicationBuilderExtension.cs
public static async Task<WebApplicationBuilder> AddAllModules(
    this WebApplicationBuilder builder)
{
    // Existing modules...
    builder.AddMyModule(connectionString);
    return builder;
}

Add an Integration Event

1

Define Event Contract

MyModule.IntegrationEvents/MyEntityCreated.cs
public class MyEntityCreated
{
    public Guid EntityId { get; init; }
    public string Name { get; init; }
    public DateTime CreatedAt { get; init; }
}
2

Publish Event

MyModule.Application/Services/MyService.cs
public async Task<Result<MyDto>> CreateAsync(...)
{
    // Create entity...
    
    var @event = new MyEntityCreated
    {
        EntityId = entity.Id,
        Name = entity.Name,
        CreatedAt = DateTime.UtcNow
    };
    
    await _eventBus.PublishAsync(@event, ct);
    
    return Result<MyDto>.Success(dto);
}
3

Create Event Handler

OtherModule.Application/EventHandlers/MyEntityCreatedHandler.cs
public class MyEntityCreatedHandler 
    : IIntegrationEventHandler<MyEntityCreated, bool>
{
    public async Task<Result<bool>> HandleAsync(
        MyEntityCreated @event, 
        CancellationToken ct)
    {
        // Handle event
        return Result<bool>.Success(true);
    }
}
4

Register Handler

OtherModule.Endpoints/Extensions/ServiceCollectionExtensions.cs
services.AddScoped<IIntegrationEventHandler<MyEntityCreated, bool>, 
    MyEntityCreatedHandler>();

Troubleshooting

Common Issues

Kill the process using the port:
# Find process
lsof -i :5000

# Kill process
kill -9 <PID>
Check connection string in .env file:
DB=Host=localhost;Port=5432;Database=wolfix;Username=postgres;Password=yourpassword
Ensure PostgreSQL is running:
docker ps | grep postgres
Reset database:
dotnet ef database drop --project {Module}.Infrastructure --startup-project Wolfix.API
dotnet ef database update --project {Module}.Infrastructure --startup-project Wolfix.API
Check dashboard URL in terminal output. Default is http://localhost:15000.Ensure no firewall is blocking the port.

Next Steps

Deployment

Learn how to deploy to production

Database Migrations

Advanced migration strategies

Aspire Orchestration

Deep dive into .NET Aspire

Build docs developers (and LLMs) love