Skip to main content
US Army Corps of EngineersInstitute for Water Resources, Risk Management Center
Draft

This web application architecture guidance is still in draft and is subject to change.

Backend Architecture

This guide defines the standard backend architecture for RMC web applications using ASP.NET Core and Entity Framework Core. It targets developers who may be new to .NET development.

RMC applications use Clean Architecture, which provides the testability, maintainability, and separation of concerns required for life safety applications.


Technology Stack

TechnologyVersionPurpose
ASP.NET CoreLatestWeb API framework
Entity Framework CoreLatestObject-relational mapper (ORM)
PostgreSQL15+Relational database
JWT Bearer-Authentication tokens
Serilog-Structured logging

C# Fundamentals for This Guide

This section explains C# syntax and concepts that appear throughout this guide. Readers already familiar with C# may skip to Architecture Overview.

Classes and Objects

A class serves as a blueprint for creating objects. An object represents an instance of a class.

// This is a class definition - a blueprint
public class Analysis
{
public Guid Id { get; set; }
public string Name { get; set; }
}

// This code creates an object (instance) from the blueprint
var myAnalysis = new Analysis();
myAnalysis.Id = Guid.NewGuid();
myAnalysis.Name = "Settlement Study";

Properties

Properties represent variables that belong to a class. The { get; set; } syntax indicates that external code can read (get) and write (set) the property.

public class Analysis
{
// External code can read and write this property
public Guid Id { get; set; }

// The "= string.Empty" sets a default value (empty string)
public string Name { get; set; } = string.Empty;
}

Access Modifiers

Access modifiers control visibility and accessibility of code:

ModifierMeaning
publicAny code can access this member
privateOnly code within the same class can access this member
protectedOnly the class and its derived classes can access this member
internalOnly code within the same project can access this member
public class SettlementController
{
// Private field - only code within this class can access it
// "readonly" means the constructor sets it once and nothing else can change it
private readonly ICalculationService _calculationService;

// Public method - any code can call this method
public void Calculate() { }
}

Constructors

A constructor is a special method that runs when code creates a new object. The constructor sets up the object's initial state.

public class CalculationService
{
private readonly IAnalysisRepository _repository;

// Constructor - same name as the class, no return type
// The framework "injects" parameters via dependency injection
public CalculationService(IAnalysisRepository repository)
{
_repository = repository; // Store the injected dependency
}
}

Interfaces

An interface defines a contract that specifies what methods a class must have, without specifying how those methods work. Similar to a job description, an interface specifies what tasks a class must perform, but not how to perform them.

// Interface - the contract (what the class must do)
// By convention, interface names start with "I"
public interface IAnalysisRepository
{
Task<Analysis?> GetByIdAsync(Guid id); // Implementers must provide this method
Task AddAsync(Analysis analysis); // Implementers must provide this method
}

// Implementation - fulfills the contract (how the class does it)
// The ": IAnalysisRepository" indicates this class implements the interface
public class AnalysisRepository : IAnalysisRepository
{
public Task<Analysis?> GetByIdAsync(Guid id)
{
// Actual code that retrieves an analysis from the database
}

public Task AddAsync(Analysis analysis)
{
// Actual code that saves an analysis to the database
}
}

Why use interfaces? Interfaces allow developers to swap implementations without changing the code that uses them. During testing, developers can provide a fake implementation that doesn't require a real database.

Inheritance

When a class inherits from another class, the child class receives all the parent's code and can add or change behavior. The : symbol indicates inheritance.

// ControllerBase is an ASP.NET class that provides helpful methods like Ok() and NotFound()
// SettlementController inherits from ControllerBase, so it receives all those methods
public class SettlementController : ControllerBase
{
public IActionResult GetAnalysis()
{
// Ok() comes from ControllerBase - this class does not define it
return Ok("Here's the data");
}
}

Async and Await

Async/await enables C# to handle operations that take time (like database queries or HTTP requests) without freezing the application.

// "async" marks a method that contains awaitable operations
// "Task" represents the return type for async methods that don't return a value
// "Task<Analysis>" represents async methods that return an Analysis
public async Task<Analysis?> GetByIdAsync(int id)
{
// "await" pauses this method until the database responds
// While waiting, the application can handle other requests
return await _db.Analyses.FindAsync(id);
}

Analogy: Consider ordering food at a restaurant. Without async, a customer stands at the counter staring at the kitchen until the food is ready. With async, the customer receives a number, sits down, and does other things while waiting—the kitchen calls the number when the food is ready.

Nullable Types

The ? after a type indicates that the value can be null (missing/empty).

// Analysis? means this method could return an Analysis object, or null if not found
public async Task<Analysis?> GetByIdAsync(int id)
{
return await _db.Analyses.FindAsync(id); // Returns null if no match exists
}

// When using nullable types, code must check for null
var analysis = await repository.GetByIdAsync(123);
if (analysis == null)
{
return NotFound(); // Handle the missing case
}
// Now the code knows analysis is not null, so it can safely use it

Lambda Expressions

Lambda expressions provide a shorthand way to write small functions. The => symbol reads as "goes to" or "maps to."

// Full method syntax
public bool IsActive(Analysis a)
{
return a.Status == AnalysisStatus.Running;
}

// Same logic as a lambda expression
a => a.Status == AnalysisStatus.Running

// Queries use lambdas - "find analyses where the UserId matches"
var analyses = await _db.Analyses
.Where(a => a.UserId == userId) // a => ... is the lambda
.ToListAsync();

Generics

Generics allow code to work with different types. The type goes in angle brackets <>.

// List<string> represents a list that holds strings
List<string> names = new List<string>();
names.Add("Alice");
names.Add("Bob");

// Task<Analysis> represents an async operation that will eventually return an Analysis
Task<Analysis> futureAnalysis = repository.GetByIdAsync(1);

// IReadOnlyList<Analysis> represents a list of analyses that code cannot modify
IReadOnlyList<Analysis> analyses = await repository.GetAllAsync();

Attributes

Attributes attach metadata tags to code elements using square brackets []. They provide extra information to the framework.

[ApiController]  // Tells ASP.NET this class is an API controller
[Route("api/analyses")] // Sets the URL path for this controller
[Authorize] // Requires authentication to access
public class AnalysisController : ControllerBase
{
[HttpGet("{id}")] // Responds to GET requests at /api/analyses/{id}
public async Task<IActionResult> GetById(Guid id)
{
// ...
}

[HttpPost] // Responds to POST requests at /api/analyses
public async Task<IActionResult> Create([FromBody] CreateAnalysisDto dto)
{
// [FromBody] tells ASP.NET to get 'dto' from the request body JSON
}
}

Namespaces

Namespaces organize code and prevent naming conflicts. They function like folders for code.

// This class lives in the WebApp.Domain.Entities namespace
namespace WebApp.Domain.Entities;

public class Analysis
{
// ...
}

// To use Analysis in another file, code either:
// 1. Uses the full name: WebApp.Domain.Entities.Analysis
// 2. Adds a "using" statement at the top of the file:
using WebApp.Domain.Entities;
// Now the code can just write: Analysis

Extension Methods

Extension methods add new methods to existing types without modifying them. They must reside in a static class and have this before the first parameter.

// Static class containing extension methods
public static class DependencyInjection
{
// "this IServiceCollection services" makes this an extension method
// It adds "AddApplication()" to IServiceCollection
public static IServiceCollection AddApplication(this IServiceCollection services)
{
services.AddScoped<ICalculationService, CalculationService>();
return services;
}
}

// Now code can call it like a regular method on IServiceCollection:
builder.Services.AddApplication();

Architecture Overview

What Is Software Architecture?

Software architecture defines how code organizes into different parts and how those parts communicate. Good architecture makes code easier to understand, test, and change over time.

Analogy: Consider building a house. A builder could combine all rooms into one large space, but that would create chaos. Instead, builders organize rooms by purpose with defined connections (doors, hallways). Software architecture applies the same principle to code.

What Is Clean Architecture?

Clean Architecture organizes code into layers, where each layer has a specific job. The most important rule states that dependencies point inward—outer layers know about inner layers, but inner layers have no knowledge of outer layers.

┌─────────────────────────────────────────────────────────────────┐
│ API Layer │
│ (Controllers, Middleware, Program.cs) │
│ Handles HTTP requests │
├─────────────────────────────────────────────────────────────────┤
│ Infrastructure Layer │
│ (DbContext, Repositories, External Services) │
│ Handles databases and external systems │
├─────────────────────────────────────────────────────────────────┤
│ Application Layer │
│ (Services, Interfaces, DTOs, Use Cases) │
│ Contains application logic and rules │
├─────────────────────────────────────────────────────────────────┤
│ Domain Layer │
│ (Entities, Business Rules, Value Objects) │
│ Contains core business logic and data │
│ │
│ ↑ NO DEPENDENCIES ↑ │
│ This layer has no knowledge of other layers │
└─────────────────────────────────────────────────────────────────┘

The Domain layer at the center has zero dependencies on any other layer, framework, or library. It contains pure C# code—business entities and rules—that developers can test without databases, HTTP, or any external system.

An Analogy for Clean Architecture

Consider a restaurant:

  • Domain Layer = Recipes and cooking techniques. These represent core knowledge that doesn't depend on anything else. A recipe for soup remains the same whether in a food truck or a five-star restaurant.

  • Application Layer = The kitchen workflow. "When an order comes in, get ingredients, follow the recipe, plate the food." This layer coordinates the work but has no knowledge of customers or payment.

  • Infrastructure Layer = The actual kitchen equipment, refrigerators, and suppliers. These are the physical tools that make the workflow possible.

  • API Layer = The waitstaff and front desk. They take orders from customers, pass them to the kitchen, and deliver the results.

If the kitchen equipment (Infrastructure) changes, the recipes (Domain) remain unchanged. If the restaurant switches from dine-in to delivery (API), the recipes still remain unchanged. That demonstrates the power of Clean Architecture.

Why Clean Architecture for RMC Applications?

RMC applications have specific requirements that make Clean Architecture the right choice:

RequirementHow Clean Architecture Helps
Life safety applicationsDevelopers can exhaustively unit test domain logic in complete isolation from databases and HTTP—critical calculations become verifiable
Multiple teamsClear project boundaries allow teams to own specific layers without conflicts
Long-term maintenanceThe dependency rule prevents architectural decay; inner layers remain stable as outer layers change
Complex calculationsEngineering calculations live in the Domain layer, protected from infrastructure concerns
Regulatory complianceSeparation makes it easier to audit and verify critical business logic

The Dependency Rule

The fundamental rule of Clean Architecture:

Inner layers cannot know about or depend on outer layers.

In practice, this means:

  • Domain depends on nothing (it's the innermost layer)
  • Application depends only on Domain
  • Infrastructure depends on Domain and Application
  • API depends on Application and Infrastructure

The compiler enforces this at compile time through project references. If a developer tries to reference Infrastructure from Domain, the compiler produces an error and the code won't compile.

Why does this matter? Core business logic (Domain) can never become "infected" by database code, HTTP code, or any other infrastructure concern. Developers can change the database from PostgreSQL to SQL Server without touching Domain code. Developers can change the API from REST to GraphQL without touching Domain code.

Layer Responsibilities

LayerWhat Lives HereWhat It Does
DomainEntities, enums, business logicDefines data structures and core business rules
ApplicationServices, interfaces, DTOsCoordinates operations, defines contracts for infrastructure
InfrastructureDbContext, repositories, typed HTTP clientsImplements data access and external integrations
APIControllers, middleware, Program.csHandles HTTP requests and responses

Key Concepts

What Is ASP.NET Core?

ASP.NET Core is Microsoft's framework for building web applications and APIs in C#. It handles:

  • Receiving HTTP requests from clients (browsers, mobile apps)
  • Routing requests to the appropriate handler code
  • Serializing/deserializing JSON data automatically
  • Managing application configuration and services

What Is an API?

An API (Application Programming Interface) provides a way for programs to communicate. In web development, it usually means HTTP endpoints that accept requests and return data (typically JSON).

Example: The frontend (React, Vue, etc.) sends an HTTP request to GET /api/analyses/123, and the backend returns JSON data like {"id": 123, "name": "Settlement Study", "status": "completed"}.

The frontend never directly accesses the database—it always goes through the API. This separation provides security and flexibility.

What Is an ORM?

An ORM (Object-Relational Mapper) enables developers to work with database records as C# objects instead of writing raw SQL.

Without an ORM:

SELECT * FROM analyses WHERE user_id = '123';

With an ORM (Entity Framework Core):

var analyses = await _db.Analyses
.Where(a => a.UserId == "123")
.ToListAsync();

The ORM translates C# code into SQL and maps the results back to C# objects.

What Is Dependency Injection?

Dependency Injection (DI) is a pattern where objects receive their dependencies (other objects they need) from the outside rather than creating them internally.

// WITHOUT dependency injection - the class creates its own dependencies
public class AnalysisController
{
private readonly AppDbContext _db;

public AnalysisController()
{
// Problem: This creates a tight coupling to a specific database
// Problem: Tests cannot substitute a fake database
_db = new AppDbContext();
}
}

// WITH dependency injection - dependencies come from outside
public class AnalysisController
{
private readonly IAnalysisRepository _repository;

public AnalysisController(IAnalysisRepository repository)
{
// The framework provides the repository
// Tests can provide a fake repository
// Production code uses the real one
_repository = repository;
}
}

Why is this important? Dependency injection is essential to Clean Architecture because:

  1. Testability: Tests can verify class behavior by providing fake dependencies
  2. Flexibility: Developers can swap implementations without changing the class
  3. The dependency rule: Outer layers provide implementations of interfaces that inner layers define

What Is a Repository?

A repository handles all database operations for a specific entity. It hides the details of how the system stores and retrieves data.

// The interface defines WHAT operations the repository provides
public interface IAnalysisRepository
{
Task<Analysis?> GetByIdAsync(Guid id);
Task<IReadOnlyList<Analysis>> GetAllForUserAsync(string userId);
Task AddAsync(Analysis analysis);
Task UpdateAsync(Analysis analysis);
Task DeleteAsync(Analysis analysis);
}

// The implementation defines HOW those operations work
public class AnalysisRepository : IAnalysisRepository
{
public async Task<Analysis?> GetByIdAsync(Guid id)
{
return await _db.Analyses.FindAsync(id);
}
// ... other methods
}

Why use repositories? They create a boundary between application logic and the database. If the database or ORM changes, only the repository implementation needs to change, not the code that uses it.

What Is a DTO?

A DTO (Data Transfer Object) is a simple object that carries data between layers, especially between the API and external clients.

// Domain entity - contains business logic and all internal properties
public class Analysis
{
public Guid Id { get; set; }
public string Name { get; set; }
public string UserId { get; set; } // Internal - the API should not expose this
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public string InternalNotes { get; set; } // Internal - the API should not expose this

public bool CanRun() { /* business logic */ }
}

// DTO - only the data API clients need to see
public class AnalysisDto
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Status { get; set; }
public DateTime CreatedAt { get; set; }
}

Why use DTOs?

  1. Security: The API does not expose internal fields like UserId or InternalNotes
  2. Stability: The API contract can remain the same even if internal entities change
  3. Shape: The API returns data in exactly the format clients need

What Is Middleware?

Middleware runs for every HTTP request, before or after controller code. It functions like a series of checkpoints that each request passes through.

HTTP Request

[Logging Middleware] ← Logs the request

[Authentication Middleware] ← Checks if the user is logged in

[Error Handling Middleware] ← Catches any errors

[Controller] ← The actual handler code

[Error Handling Middleware] ← Can modify the response if an error occurred

[Logging Middleware] ← Logs the response

HTTP Response

Project Structure

Clean Architecture uses multiple projects to enforce the dependency rule at compile time. Each project becomes a separate .csproj file.

Why Multiple Projects?

When code adds a using statement or reference to another project, the compiler checks if that reference is allowed based on the project's dependencies. If the Domain project doesn't have a reference to Infrastructure, the compiler rejects any attempt to use Infrastructure code in Domain.

This compile-time enforcement prevents accidental violations of the architecture.

The Project Layout

backend/
├── src/
│ ├── WebApp.Domain/ # Innermost layer - no dependencies
│ │ ├── Entities/ # Business objects
│ │ │ ├── User.cs
│ │ │ └── Analysis.cs
│ │ ├── Enums/ # Status codes, categories, etc.
│ │ │ └── AnalysisStatus.cs
│ │ └── WebApp.Domain.csproj # Project file - no references
│ │
│ ├── WebApp.Application/ # References: Domain only
│ │ ├── Interfaces/ # Contracts for infrastructure
│ │ │ ├── IAnalysisRepository.cs
│ │ │ └── ICalculationService.cs
│ │ ├── Services/ # Application logic
│ │ │ └── CalculationService.cs
│ │ ├── DTOs/ # Data shapes for API
│ │ │ ├── AnalysisDto.cs
│ │ │ └── ApiResponse.cs
│ │ └── WebApp.Application.csproj
│ │
│ ├── WebApp.Infrastructure/ # References: Domain, Application
│ │ ├── Data/
│ │ │ ├── AppDbContext.cs # Database connection
│ │ │ └── Configurations/ # How entities map to tables
│ │ │ ├── UserConfiguration.cs
│ │ │ └── AnalysisConfiguration.cs
│ │ ├── Repositories/ # Data access implementations
│ │ │ └── AnalysisRepository.cs
│ │ ├── Clients/ # Typed HTTP clients for external services
│ │ │ └── CalculationClient.cs
│ │ └── WebApp.Infrastructure.csproj
│ │
│ └── WebApp.API/ # References: Application, Infrastructure
│ ├── Controllers/ # HTTP endpoints
│ │ ├── AuthController.cs
│ │ └── Analyses/
│ │ ├── SettlementController.cs
│ │ └── SeepageController.cs
│ ├── Middleware/ # Request/response processing
│ │ └── ErrorHandlingMiddleware.cs
│ ├── Program.cs # Application entry point
│ ├── appsettings.json # Configuration
│ └── WebApp.API.csproj

├── tests/
│ ├── WebApp.Domain.Tests/ # Unit tests for domain logic
│ ├── WebApp.Application.Tests/ # Unit tests for services
│ └── WebApp.Integration.Tests/ # Tests with real database

└── WebApp.sln # Solution file - groups all projects

Project References

Each .csproj file declares which other projects it can reference. This enforces the dependency rule.

<!-- WebApp.Domain.csproj - NO project references (innermost layer) -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <!-- Always target latest .NET -->
</PropertyGroup>
<!-- Notice: No ProjectReference elements - Domain depends on nothing -->
</Project>
<!-- WebApp.Application.csproj - Can only reference Domain -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<!-- This project can use code from Domain -->
<ProjectReference Include="..\WebApp.Domain\WebApp.Domain.csproj" />
</ItemGroup>
</Project>
<!-- WebApp.Infrastructure.csproj - References Domain and Application -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\WebApp.Domain\WebApp.Domain.csproj" />
<ProjectReference Include="..\WebApp.Application\WebApp.Application.csproj" />
<!-- NuGet packages - always use latest stable versions -->
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.*" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.*" />
</ItemGroup>
</Project>
<!-- WebApp.API.csproj - References Application and Infrastructure -->
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\WebApp.Application\WebApp.Application.csproj" />
<ProjectReference Include="..\WebApp.Infrastructure\WebApp.Infrastructure.csproj" />
</ItemGroup>
</Project>
note

These examples show .NET 9 as the current latest version. Always target the latest stable .NET release when starting or upgrading a project. Update the TargetFramework and package versions accordingly.


The Domain Layer

The Domain layer forms the heart of the application. It contains business entities and rules. It has no dependencies on any other project, framework, or external library.

What Are Entities?

Entities are classes that represent the core objects in the business domain. In RMC applications, this might include Analysis, User, Project, or CalculationResult.

Domain entities are POCOs (Plain Old CLR Objects)—simple C# classes with no special framework code.

Entity Example

// Domain/Entities/Analysis.cs
namespace WebApp.Domain.Entities;

/// <summary>
/// Represents an engineering analysis in the system.
/// This is a domain entity - it contains business data and logic.
/// </summary>
public class Analysis
{
// Properties - the data this entity holds
// UUID primary key - generated by PostgreSQL via gen_random_uuid()
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string UserId { get; set; } = string.Empty;
public AnalysisStatus Status { get; set; } = AnalysisStatus.Draft;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;

// Domain logic - business rules that apply to this entity

/// <summary>
/// Determines if this analysis can run based on its current status.
/// Business rule: An analysis cannot run if it's already running or archived.
/// </summary>
public bool CanRun()
{
return Status != AnalysisStatus.Running
&& Status != AnalysisStatus.Archived;
}

/// <summary>
/// Marks the analysis as currently running.
/// Throws an exception if the business rules don't allow it.
/// </summary>
public void MarkAsRunning()
{
// Enforce business rule
if (!CanRun())
{
throw new InvalidOperationException(
"Analysis cannot run in its current state.");
}

Status = AnalysisStatus.Running;
UpdatedAt = DateTime.UtcNow;
}

/// <summary>
/// Marks the analysis as completed.
/// </summary>
public void Complete()
{
Status = AnalysisStatus.Completed;
UpdatedAt = DateTime.UtcNow;
}
}
// Domain/Enums/AnalysisStatus.cs
namespace WebApp.Domain.Enums;

/// <summary>
/// Represents the possible states an analysis can have.
/// </summary>
public enum AnalysisStatus
{
Draft, // Just created, not yet run
Running, // Currently calculating
Completed, // Finished successfully
Failed, // Finished with errors
Archived // No longer active
}

Why No Database Attributes?

Some codebases use code like this:

// DON'T do this in Clean Architecture
public class Analysis
{
[Key] // Database attribute
public Guid Id { get; set; }

[Required] // Database attribute
[MaxLength(255)] // Database attribute
public string Name { get; set; }
}

In Clean Architecture, entities don't have these database attributes because:

  1. The Domain layer has no knowledge of databases. It contains pure business logic.
  2. Testability: Developers can create and test entities without any database setup.
  3. Flexibility: Switching from Entity Framework to a different ORM (or no ORM) requires no changes to Domain code.

Instead, the Infrastructure layer configures database mapping separately.


The Application Layer

The Application layer sits between the API and Domain. It contains:

  • Interfaces that define what operations the system provides (contracts)
  • Services that coordinate business operations
  • DTOs that define data shapes for the API

Repository Interface

The Application layer defines interfaces for data access. It specifies "the system needs a way to get and save analyses" without specifying how.

// Application/Interfaces/IAnalysisRepository.cs
namespace WebApp.Application.Interfaces;

/// <summary>
/// Defines the operations available for Analysis data access.
/// The Infrastructure layer provides the actual implementation.
/// </summary>
public interface IAnalysisRepository
{
/// <summary>
/// Gets an analysis by its ID.
/// Returns null if not found.
/// </summary>
Task<Analysis?> GetByIdAsync(Guid id);

/// <summary>
/// Gets an analysis by ID, but only if it belongs to the specified user.
/// Returns null if not found or if the user doesn't own it.
/// </summary>
Task<Analysis?> GetByIdForUserAsync(Guid id, string userId);

/// <summary>
/// Gets all analyses belonging to a user.
/// </summary>
Task<IReadOnlyList<Analysis>> GetAllForUserAsync(string userId);

/// <summary>
/// Adds a new analysis to the database.
/// </summary>
Task AddAsync(Analysis analysis);

/// <summary>
/// Updates an existing analysis in the database.
/// </summary>
Task UpdateAsync(Analysis analysis);

/// <summary>
/// Deletes an analysis from the database.
/// </summary>
Task DeleteAsync(Analysis analysis);
}

Service Interface and Implementation

Services contain application logic—they coordinate between repositories, domain entities, and external systems.

// Application/Interfaces/ICalculationService.cs
namespace WebApp.Application.Interfaces;

/// <summary>
/// Defines operations for running engineering calculations.
/// </summary>
public interface ICalculationService
{
/// <summary>
/// Runs the calculation for the specified analysis.
/// </summary>
/// <param name="analysisId">The ID of the analysis to run</param>
/// <param name="userId">The ID of the user making the request (for authorization)</param>
/// <returns>A response indicating success, failure, or missing inputs</returns>
Task<ApiResponse<object>> RunAsync(Guid analysisId, string userId);
}
// Application/Services/CalculationService.cs
namespace WebApp.Application.Services;

/// <summary>
/// Implements the calculation service.
/// This class coordinates the workflow for running an analysis.
/// </summary>
public class CalculationService : ICalculationService
{
// Dependencies - the constructor receives these via dependency injection
private readonly IAnalysisRepository _repository;
private readonly ILogger<CalculationService> _logger;

// Constructor - the framework provides dependencies via dependency injection
public CalculationService(
IAnalysisRepository repository,
ILogger<CalculationService> logger)
{
_repository = repository;
_logger = logger;
}

public async Task<ApiResponse<object>> RunAsync(Guid analysisId, string userId)
{
// Step 1: Load the analysis from the database
// Note: The code uses the repository interface, not the database directly
var analysis = await _repository.GetByIdForUserAsync(analysisId, userId);

// Step 2: Handle "not found" case
if (analysis == null)
{
return new ApiResponse<object>
{
Status = "error",
Errors = ["Analysis not found"]
};
}

// Step 3: Use domain logic to check business rules
// The CanRun() method lives on the entity - it's domain logic
if (!analysis.CanRun())
{
return new ApiResponse<object>
{
Status = "error",
Errors = ["Analysis cannot run in its current state"]
};
}

// Step 4: Update domain state using domain methods
analysis.MarkAsRunning();
await _repository.UpdateAsync(analysis);

// Step 5: Log the operation
_logger.LogInformation(
"[CALC] Running analysis {AnalysisId} for user {UserId}",
analysisId, userId);

// Step 6: Run the actual calculation
// (In a real application, this calls the calculation library via HTTP client)
// ... calculation logic ...

// Step 7: Update domain state with results
analysis.Complete();
await _repository.UpdateAsync(analysis);

// Step 8: Return success response
return new ApiResponse<object>
{
Status = "success",
CanRun = true
};
}
}

Data Transfer Objects (DTOs)

DTOs are simple classes that define the shape of data going in and out of the API.

// Application/DTOs/AnalysisDto.cs
namespace WebApp.Application.DTOs;

/// <summary>
/// Represents analysis data returned to API clients.
/// Only includes fields that external clients should see.
/// </summary>
public class AnalysisDto
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}

/// <summary>
/// Data required to create a new analysis.
/// </summary>
public class CreateAnalysisDto
{
public string Name { get; set; } = string.Empty;
}

/// <summary>
/// Data that the API can update on an existing analysis.
/// </summary>
public class UpdateAnalysisDto
{
public string Name { get; set; } = string.Empty;
}
// Application/DTOs/ApiResponse.cs
namespace WebApp.Application.DTOs;

/// <summary>
/// Standard response envelope for all API operations.
/// This consistent format simplifies frontend development.
///
/// This is the ONE class that uses [JsonPropertyName] attributes to ensure
/// the envelope fields are always serialized with the exact expected names.
/// All other DTOs rely on the global camelCase naming policy (see JSON Serialization below).
/// </summary>
public class ApiResponse<T>
{
[JsonPropertyName("status")]
public string Status { get; set; } = "success";

[JsonPropertyName("inputs")]
public List<string> Inputs { get; set; } = [];

[JsonPropertyName("warnings")]
public List<string> Warnings { get; set; } = [];

[JsonPropertyName("errors")]
public List<string> Errors { get; set; } = [];

[JsonPropertyName("canRun")]
public bool CanRun { get; set; } = true;

[JsonPropertyName("data")]
public T? Data { get; set; }

[JsonPropertyName("resultsCleared")]
public bool ResultsCleared { get; set; } = false;
}
Why [JsonPropertyName] only on ApiResponse<T>?

ASP.NET Core's default JSON serialization uses camelCase naming automatically (Statusstatus, CanRuncanRun). This means most DTOs need no JSON attributes at all—the framework handles casing. The ApiResponse<T> envelope is the exception: it uses explicit [JsonPropertyName] attributes to guarantee the contract is never accidentally broken, since every frontend depends on these exact field names.

JSON Serialization Strategy

ASP.NET Core serializes JSON using camelCase by default. This means a C# property like CanRun automatically becomes canRun in JSON—no attributes needed on most DTOs.

The rules:

  1. Do not add [JsonPropertyName] attributes to regular DTOs. The framework's default camelCase policy handles them.
  2. Do add [JsonPropertyName] attributes to the ApiResponse<T> envelope class. This guarantees the standard messaging contract is never accidentally broken.
  3. Do use a JsonDefaults singleton for any manual serialization (e.g., serializing to a JSONB column or a message queue).
// Infrastructure/JsonDefaults.cs
namespace WebApp.Infrastructure;

/// <summary>
/// Shared JSON serialization options for manual serialization scenarios.
/// ASP.NET controller serialization uses its own defaults (which are also camelCase).
/// Use this for serializing to JSONB columns, message queues, or HTTP client payloads.
/// </summary>
public static class JsonDefaults
{
public static readonly JsonSerializerOptions CamelCase = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
}

// Usage example - serializing data to a JSONB column:
var json = JsonSerializer.Serialize(analysisData, JsonDefaults.CamelCase);

The Infrastructure Layer

The Infrastructure layer contains implementations for data access, external services, and typed HTTP clients. This is where Entity Framework Core, database connections, and external API clients live.

What Is a DbContext?

A DbContext is Entity Framework Core's main class for interacting with the database. It represents a session with the database and enables querying and saving data.

// Infrastructure/Data/AppDbContext.cs
namespace WebApp.Infrastructure.Data;

/// <summary>
/// The database context for this application.
/// It serves as the "gateway" to the database - all database operations go through here.
/// </summary>
public class AppDbContext : DbContext
{
// Constructor - receives configuration from dependency injection
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options) // Pass options to the parent class
{
}

// DbSet<T> represents a table in the database
// This creates a "Users" table mapped to the User entity
public DbSet<User> Users => Set<User>();

// This creates an "Analyses" table mapped to the Analysis entity
public DbSet<Analysis> Analyses => Set<Analysis>();

/// <summary>
/// The framework calls this method when creating the model.
/// This is where entity-to-table mappings get configured.
/// </summary>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Find all configuration classes in this project and apply them
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
}

Entity Configuration (Fluent API)

Instead of putting database attributes on domain entities, developers use Fluent API to configure the mapping in separate configuration classes.

// Infrastructure/Data/Configurations/AnalysisConfiguration.cs
namespace WebApp.Infrastructure.Data.Configurations;

/// <summary>
/// Configures how the Analysis entity maps to the database.
/// This keeps database concerns out of the Domain layer.
/// </summary>
public class AnalysisConfiguration : IEntityTypeConfiguration<Analysis>
{
public void Configure(EntityTypeBuilder<Analysis> builder)
{
// Table name in the database
builder.ToTable("analyses");

// Primary key - UUID generated by PostgreSQL
builder.HasKey(a => a.Id);
builder.Property(a => a.Id)
.HasDefaultValueSql("gen_random_uuid()");

// Property configurations
builder.Property(a => a.Name)
.IsRequired() // NOT NULL in database
.HasMaxLength(255); // VARCHAR(255)

builder.Property(a => a.UserId)
.IsRequired();

// Store enum as string (e.g., "Draft", "Running") instead of number
builder.Property(a => a.Status)
.HasConversion<string>()
.HasMaxLength(50);

// Set default value using SQL
builder.Property(a => a.CreatedAt)
.HasDefaultValueSql("NOW()");

// Create an index for faster queries by UserId
builder.HasIndex(a => a.UserId);

// Define relationship: Each Analysis belongs to one User
builder.HasOne<User>()
.WithMany()
.HasForeignKey(a => a.UserId);
}
}

Repository Implementation

The repository class implements the interface defined in Application, using DbContext for actual database operations.

// Infrastructure/Repositories/AnalysisRepository.cs
namespace WebApp.Infrastructure.Repositories;

/// <summary>
/// Implementation of IAnalysisRepository using Entity Framework Core.
/// </summary>
public class AnalysisRepository : IAnalysisRepository
{
private readonly AppDbContext _db;

public AnalysisRepository(AppDbContext db)
{
_db = db;
}

public async Task<Analysis?> GetByIdAsync(Guid id)
{
// FindAsync looks up by primary key
return await _db.Analyses.FindAsync(id);
}

public async Task<Analysis?> GetByIdForUserAsync(Guid id, string userId)
{
// FirstOrDefaultAsync returns the first match or null
// The lambda expression filters: where Id matches AND UserId matches
return await _db.Analyses
.FirstOrDefaultAsync(a => a.Id == id && a.UserId == userId);
}

public async Task<IReadOnlyList<Analysis>> GetAllForUserAsync(string userId)
{
return await _db.Analyses
.Where(a => a.UserId == userId) // Filter by user
.OrderByDescending(a => a.UpdatedAt) // Newest first
.ToListAsync(); // Execute query
}

public async Task AddAsync(Analysis analysis)
{
// Add tells EF to track this entity for insertion
_db.Analyses.Add(analysis);
// SaveChangesAsync sends the INSERT to the database
await _db.SaveChangesAsync();
}

public async Task UpdateAsync(Analysis analysis)
{
// Update tells EF this entity has been modified
_db.Analyses.Update(analysis);
// SaveChangesAsync sends the UPDATE to the database
await _db.SaveChangesAsync();
}

public async Task DeleteAsync(Analysis analysis)
{
// Remove tells EF to delete this entity
_db.Analyses.Remove(analysis);
// SaveChangesAsync sends the DELETE to the database
await _db.SaveChangesAsync();
}
}

Registering Services with Dependency Injection

Each layer provides an extension method that registers its services with the dependency injection container.

// Infrastructure/DependencyInjection.cs
namespace WebApp.Infrastructure;

/// <summary>
/// Extension methods for registering Infrastructure layer services.
/// </summary>
public static class DependencyInjection
{
/// <summary>
/// Adds Infrastructure services to the dependency injection container.
/// Call this in Program.cs: builder.Services.AddInfrastructure(configuration)
/// </summary>
public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
IConfiguration configuration)
{
// Register the database context
// When code requests AppDbContext, the framework creates one connected to PostgreSQL
services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(configuration.GetConnectionString("DefaultConnection")));

// Register repositories
// When code requests IAnalysisRepository, the framework provides an AnalysisRepository
// "Scoped" means the framework creates one instance per HTTP request
services.AddScoped<IAnalysisRepository, AnalysisRepository>();

return services;
}
}
// Application/DependencyInjection.cs
namespace WebApp.Application;

/// <summary>
/// Extension methods for registering Application layer services.
/// </summary>
public static class DependencyInjection
{
/// <summary>
/// Adds Application services to the dependency injection container.
/// Call this in Program.cs: builder.Services.AddApplication()
/// </summary>
public static IServiceCollection AddApplication(this IServiceCollection services)
{
// Register services
services.AddScoped<ICalculationService, CalculationService>();

return services;
}
}

Typed HTTP Clients

When the backend needs to communicate with external services (such as calculation libraries exposed as HTTP APIs), use typed HTTP clients registered through IHttpClientFactory.

// Application/Interfaces/ICalculationClient.cs
namespace WebApp.Application.Interfaces;

/// <summary>
/// Defines the contract for calling the calculation service HTTP API.
/// The interface lives in Application; the implementation lives in Infrastructure.
/// </summary>
public interface ICalculationClient
{
Task<CalculationResult> RunAnalysisAsync(CalculationRequest request);
}
// Infrastructure/Clients/CalculationClient.cs
namespace WebApp.Infrastructure.Clients;

/// <summary>
/// Typed HTTP client for communicating with the calculation service.
/// Uses IHttpClientFactory for proper connection management.
/// </summary>
public class CalculationClient : ICalculationClient
{
private readonly HttpClient _http;
private readonly ILogger<CalculationClient> _logger;

public CalculationClient(HttpClient http, ILogger<CalculationClient> logger)
{
_http = http;
_logger = logger;
}

public async Task<CalculationResult> RunAnalysisAsync(CalculationRequest request)
{
_logger.LogInformation("[CALC] Sending calculation request");

var response = await _http.PostAsJsonAsync("/api/calculate", request, JsonDefaults.CamelCase);
response.EnsureSuccessStatusCode();

return await response.Content.ReadFromJsonAsync<CalculationResult>(JsonDefaults.CamelCase)
?? throw new InvalidOperationException("Calculation service returned null");
}
}

Register the typed client in the Infrastructure DI extension:

// In Infrastructure/DependencyInjection.cs
services.AddHttpClient<ICalculationClient, CalculationClient>(client =>
{
client.BaseAddress = new Uri(configuration["Services:Calculation:BaseUrl"]!);
client.Timeout = TimeSpan.FromMinutes(5); // Calculations can take time
});

This pattern applies to any external HTTP service the backend consumes (calculation libraries, System-Response, third-party APIs). Each service gets its own interface in Application and its own typed client in Infrastructure/Clients/.


The API Layer

The API layer handles HTTP requests and responses. It contains controllers (which handle specific routes), middleware (which processes all requests), and the application entry point.

What Is a Controller?

A controller handles HTTP requests for a specific resource. Each public method (called an action) handles a specific HTTP method and route.

// API/Controllers/Analyses/SettlementController.cs
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace WebApp.API.Controllers.Analyses;

// Attributes provide metadata to ASP.NET

[ApiController] // This class is an API controller (enables automatic validation, etc.)
[Route("api/analyses/settlement")] // Base URL path for all actions
[Authorize] // All actions in this controller require authentication
public class SettlementController : ControllerBase // Inherit from ControllerBase
{
// Dependencies - the constructor receives these via dependency injection
private readonly ICalculationService _calculationService;
private readonly IAnalysisRepository _repository;
private readonly ILogger<SettlementController> _logger;

// Constructor - ASP.NET provides these automatically via dependency injection
public SettlementController(
ICalculationService calculationService,
IAnalysisRepository repository,
ILogger<SettlementController> logger)
{
_calculationService = calculationService;
_repository = repository;
_logger = logger;
}

/// <summary>
/// Gets an analysis by ID.
/// HTTP: GET /api/analyses/settlement/{id}
/// Example: GET /api/analyses/settlement/123
/// </summary>
[HttpGet("{id}")] // {id} is a route parameter
public async Task<IActionResult> GetById(Guid id) // id comes from the URL
{
// Get the current user's ID from their authentication token
var userId = GetCurrentUserId();

// Use the repository to get the analysis
var analysis = await _repository.GetByIdForUserAsync(id, userId);

// If not found, return 404 Not Found
if (analysis == null)
return NotFound();

// Convert to DTO and return 200 OK with the data
return Ok(MapToDto(analysis));
}

/// <summary>
/// Runs the calculation for an analysis.
/// HTTP: POST /api/analyses/settlement/{id}/calculate
/// Example: POST /api/analyses/settlement/123/calculate
/// </summary>
[HttpPost("{id}/calculate")]
public async Task<IActionResult> Calculate(Guid id)
{
// Log the request
_logger.LogInformation("[API] POST calculate/{AnalysisId}", id);

var userId = GetCurrentUserId();

// Delegate to the service layer - the controller stays thin
var response = await _calculationService.RunAsync(id, userId);

// Return appropriate HTTP status based on the result
// This is a "switch expression" - a compact way to choose based on a value
return response.Status switch
{
"success" => Ok(response), // 200
"incomplete" => Ok(response), // 200
"error" => BadRequest(response), // 400
_ => StatusCode(500, response) // 500 (underscore means "anything else")
};
}

/// <summary>
/// Helper method that gets the current user's ID from their JWT token.
/// </summary>
private string GetCurrentUserId()
{
// User.FindFirstValue gets a claim from the JWT token
// ?? means "if null, throw the exception instead"
return User.FindFirstValue(ClaimTypes.NameIdentifier)
?? throw new UnauthorizedAccessException();
}

/// <summary>
/// Converts a domain entity to a DTO for the API response.
/// </summary>
private static AnalysisDto MapToDto(Analysis analysis) => new()
{
Id = analysis.Id,
Name = analysis.Name,
Status = analysis.Status.ToString(),
CreatedAt = analysis.CreatedAt,
UpdatedAt = analysis.UpdatedAt
};
}

Program.cs - The Entry Point

Program.cs is where the application starts. It wires together all the layers and configures the HTTP pipeline.

// API/Program.cs
using Serilog;
using WebApp.Application;
using WebApp.Infrastructure;

// Create the web application builder
var builder = WebApplication.CreateBuilder(args);

// === CONFIGURE LOGGING ===
// Serilog provides structured logging
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.Enrich.FromLogContext()
.WriteTo.Console()
.WriteTo.File("logs/app.log", rollingInterval: RollingInterval.Day)
.CreateLogger();

builder.Host.UseSerilog();

// === REGISTER SERVICES (Dependency Injection) ===
// These extension methods come from the Application and Infrastructure layers
builder.Services.AddApplication();
builder.Services.AddInfrastructure(builder.Configuration);

// === CONFIGURE AUTHENTICATION ===
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
};
});

builder.Services.AddAuthorization();
builder.Services.AddControllers();

// Build the application
var app = builder.Build();

// === CONFIGURE THE HTTP PIPELINE ===
// Middleware runs in the order added

// Error handling middleware catches exceptions and returns consistent error responses
app.UseMiddleware<ErrorHandlingMiddleware>();

// Authentication and authorization middleware
app.UseAuthentication();
app.UseAuthorization();

// Map controller routes
app.MapControllers();

// Start the application
app.Run();

HTTP Methods and Routes

OperationHTTP MethodRoute ExampleWhat It Does
Get oneGET/api/analyses/123Retrieves a single analysis
Get manyGET/api/analysesRetrieves a list of analyses
CreatePOST/api/analysesCreates a new analysis
UpdatePUT/api/analyses/123Updates an existing analysis
DeleteDELETE/api/analyses/123Deletes an analysis
Run actionPOST/api/analyses/123/calculateTriggers a calculation

Route Parameters and Model Binding

ASP.NET Core automatically extracts data from requests and passes it to methods:

// From URL path: GET /api/analyses/{id}
// The {id} in the route becomes the 'id' parameter
[HttpGet("{id}")]
public async Task<IActionResult> Get(Guid id)

// From query string: GET /api/analyses?page=1&limit=10
// [FromQuery] tells ASP.NET to look in the query string
[HttpGet]
public async Task<IActionResult> List(
[FromQuery] int page = 1, // Default value if not provided
[FromQuery] int limit = 10)

// From request body: POST /api/analyses with JSON body
// [FromBody] tells ASP.NET to deserialize the JSON body into this object
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateAnalysisDto dto)

// Combined: PUT /api/analyses/{id} with JSON body
[HttpPut("{id}")]
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateAnalysisDto dto)

Error Handling

Global error handling middleware catches exceptions and returns consistent error responses.

// API/Middleware/ErrorHandlingMiddleware.cs
namespace WebApp.API.Middleware;

/// <summary>
/// Middleware that catches unhandled exceptions and returns consistent error responses.
/// This runs for every request and wraps the rest of the pipeline in a try-catch.
/// </summary>
public class ErrorHandlingMiddleware
{
// _next represents the next middleware in the pipeline
private readonly RequestDelegate _next;
private readonly ILogger<ErrorHandlingMiddleware> _logger;

public ErrorHandlingMiddleware(
RequestDelegate next,
ILogger<ErrorHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}

/// <summary>
/// The framework calls this method for every HTTP request.
/// </summary>
public async Task InvokeAsync(HttpContext context)
{
try
{
// Call the next middleware (eventually reaches the controller)
await _next(context);
}
catch (Exception ex)
{
// If any exception occurs, the middleware catches it here
_logger.LogError(ex, "[ERROR] Unhandled exception");
await HandleExceptionAsync(context, ex);
}
}

/// <summary>
/// Converts an exception into an HTTP response.
/// </summary>
private static async Task HandleExceptionAsync(HttpContext context, Exception ex)
{
context.Response.ContentType = "application/json";

// Choose HTTP status code based on exception type
context.Response.StatusCode = ex switch
{
UnauthorizedAccessException => StatusCodes.Status403Forbidden,
ArgumentException => StatusCodes.Status400BadRequest,
_ => StatusCodes.Status500InternalServerError
};

// Return a consistent error response format
var response = new ApiResponse<object>
{
Status = "error",
Errors = [ex.Message]
};

await context.Response.WriteAsJsonAsync(response);
}
}

Authentication

RMC applications use JWT (JSON Web Token) bearer authentication.

What Is JWT?

A JWT (JSON Web Token) is a signed token that contains information about the user. When a user logs in, the system issues a JWT. The client includes this token in the Authorization header of every subsequent request:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

The server validates the token's signature to verify:

  1. This server issued the token (not forged)
  2. No one has tampered with the token
  3. The token has not expired

This enables stateless authentication—the server doesn't need to look up session data in a database for every request.

Getting the Current User

// In a controller, code can access the current user's information
// from the JWT token claims

private string GetCurrentUserId()
{
// User is a property on ControllerBase
// FindFirstValue looks for a claim in the JWT token
// ClaimTypes.NameIdentifier is the standard claim for user ID
return User.FindFirstValue(ClaimTypes.NameIdentifier)
?? throw new UnauthorizedAccessException("User ID not found in token");
}

Logging

All operations use structured logging with Serilog.

What Is Structured Logging?

Structured logging captures data as named properties rather than just text. This makes logs searchable and analyzable.

// BAD: String interpolation - data gets buried in text
_logger.LogInformation($"Running analysis {analysisId} for user {userId}");
// Output: "Running analysis 123 for user user-456"
// Searching for all logs about analysis 123 becomes difficult

// GOOD: Structured logging - the logger captures data as properties
_logger.LogInformation(
"[CALC] Running analysis {AnalysisId} for user {UserId}",
analysisId, userId);
// Output: "[CALC] Running analysis 123 for user user-456"
// Also captures: { AnalysisId: 123, UserId: "user-456" }
// Searching/filtering by AnalysisId or UserId becomes easy

Log Prefixes

Consistent prefixes categorize log messages:

PrefixUsage
[API]HTTP request/response handling
[CALC]Engineering calculations
[VALID]Input validation
[DB]Database operations
[ERROR]Error conditions

Testing

Clean Architecture enables comprehensive testing because each layer can operate independently.

Domain Layer Tests

Domain logic is pure C#—no database, no HTTP, no mocking needed.

// tests/WebApp.Domain.Tests/AnalysisTests.cs
public class AnalysisTests
{
[Fact] // Marks this as a test method
public void CanRun_WhenStatusIsDraft_ReturnsTrue()
{
// Arrange: Set up the test scenario
var analysis = new Analysis { Status = AnalysisStatus.Draft };

// Act & Assert: Call the method and verify the result
Assert.True(analysis.CanRun());
}

[Fact]
public void CanRun_WhenStatusIsRunning_ReturnsFalse()
{
var analysis = new Analysis { Status = AnalysisStatus.Running };

Assert.False(analysis.CanRun());
}

[Fact]
public void MarkAsRunning_WhenCannotRun_ThrowsException()
{
var analysis = new Analysis { Status = AnalysisStatus.Running };

// Assert.Throws verifies that the code throws an exception
Assert.Throws<InvalidOperationException>(() => analysis.MarkAsRunning());
}
}

Application Layer Tests

Service tests use mocked repositories—fake implementations that return predetermined data.

// tests/WebApp.Application.Tests/CalculationServiceTests.cs
public class CalculationServiceTests
{
[Fact]
public async Task RunAsync_WhenAnalysisNotFound_ReturnsError()
{
// Arrange: Create a mock repository that returns null
var mockRepo = new Mock<IAnalysisRepository>();
mockRepo
.Setup(r => r.GetByIdForUserAsync(It.IsAny<Guid>(), It.IsAny<string>()))
.ReturnsAsync((Analysis?)null); // Simulate "not found"

var service = new CalculationService(
mockRepo.Object,
Mock.Of<ILogger<CalculationService>>());

// Act: Call the method under test
var result = await service.RunAsync(1, "user-123");

// Assert: Verify the result
Assert.Equal("error", result.Status);
Assert.Contains("not found", result.Errors[0]);
}
}

Best Practices

Do

  • Keep the Domain layer free of all external dependencies
  • Keep controllers thin—delegate business logic to services
  • Use async/await for all I/O operations (database, HTTP, file system)
  • Define interfaces in Application, implement them in Infrastructure
  • Use the Repository pattern to abstract data access
  • Log all significant operations with structured logging
  • Return appropriate HTTP status codes
  • Write unit tests for Domain logic without mocks

Don't

  • Reference Infrastructure or API from Domain (violates dependency rule)
  • Put business logic in controllers (controllers should only handle HTTP)
  • Expose domain entities directly to the API (use DTOs)
  • Put database attributes on domain entities (use Fluent API configuration)
  • Catch and swallow exceptions silently (always log errors)
  • Use synchronous database calls (blocks threads, hurts performance)
  • Store secrets (passwords, API keys) in code (use configuration)
  • Skip authorization checks (security critical)

Glossary

TermDefinition
APIApplication Programming Interface—a way for programs to communicate, typically over HTTP
Async/AwaitC# keywords for handling long-running operations without blocking
AttributeMetadata attached to code elements using [brackets]
Clean ArchitectureA software design pattern where dependencies point inward toward business logic
ConstructorA special method that runs when creating a new object
ControllerA class that handles HTTP requests in ASP.NET Core
DbContextEntity Framework Core's class for database interaction
Dependency InjectionA pattern where objects receive their dependencies from outside
DTOData Transfer Object—a simple object for carrying data between layers
EntityA class representing a business object, often mapped to a database table
Extension MethodA method that adds functionality to an existing type
Fluent APIEF Core's method-chaining approach to configure entity mappings
GenericA type that works with different types, specified in angle brackets <>
InterfaceA contract defining what methods a class must implement
JWTJSON Web Token—a signed token containing user identity information
Lambda ExpressionA shorthand for writing small functions using =>
LayerA logical grouping of code with specific responsibilities
MiddlewareCode that runs for every HTTP request in the pipeline
NamespaceA way to organize code and prevent naming conflicts
NuGet PackageA library published for use in .NET projects
ORMObject-Relational Mapper—a tool that maps database records to objects
POCOPlain Old CLR Object—a simple class with no framework dependencies
PropertyA class member that encapsulates a field with get/set accessors
RepositoryA class that handles data access for a specific entity type
ServiceA class containing business or application logic