Skip to main content

.NET REST Service Basic

A production-ready .NET REST service archetype that provides a complete foundation for building scalable HTTP APIs with Entity Framework Core, comprehensive testing, and Docker support.

Overview

This archetype generates a fully-functional .NET REST service with modern patterns and practices. It includes Entity Framework Core for data access, comprehensive testing with Testcontainers, and production-ready features like monitoring, configuration management, and containerization.

Technology Stack

  • .NET 8: Latest long-term support version
  • ASP.NET Core: Web API framework
  • Entity Framework Core: Object-relational mapping
  • PostgreSQL: Primary database
  • Testcontainers .NET: Integration testing
  • Docker: Containerization
  • k6: Load testing
  • Tilt: Local Kubernetes development
  • OpenAPI/Swagger: API documentation

Key Features

Core Functionality

  • RESTful CRUD Operations: Complete Create, Read, Update, Delete operations
  • Entity Framework Integration: Code-first database approach
  • Data Validation: Comprehensive input validation
  • Error Handling: Standardized error responses
  • API Documentation: Automatic OpenAPI/Swagger generation

Production Features

  • Health Checks: Application and database health monitoring
  • Logging: Structured logging with Serilog
  • Configuration: Environment-based configuration management
  • Metrics: Application performance monitoring
  • Docker Support: Multi-stage Docker builds

Testing

  • Unit Tests: Comprehensive unit test coverage
  • Integration Tests: Database integration testing with Testcontainers
  • Load Testing: k6 performance tests for HTTP endpoints
  • Test Isolation: Isolated test environments

Project Structure

{{ prefix-name }}-{{ suffix-name }}/
├── {{ PrefixName }}{{ SuffixName }}.API/ # Web API project
│ ├── Dtos/ # Data transfer objects
│ │ ├── {{ PrefixName }}Dto.cs
│ │ ├── Create{{ PrefixName }}Response.cs
│ │ ├── Get{{ PrefixName }}Request.cs
│ │ └── Get{{ PrefixName }}Response.cs
│ ├── Logger/ # Logging infrastructure
│ │ ├── LogFactory.cs
│ │ ├── LoggingContext.cs
│ │ └── LoggingExtensions.cs
│ ├── I{{ PrefixName }}{{ SuffixName }}Service.cs
│ └── {{ PrefixName }}{{ SuffixName }}.API.csproj
├── {{ PrefixName }}{{ SuffixName }}.Client/ # Client SDK
│ ├── {{ PrefixName }}{{ SuffixName }}Client.cs
│ └── {{ PrefixName }}{{ SuffixName }}.Client.csproj
├── {{ PrefixName }}{{ SuffixName }}.Core/ # Business logic
│ ├── Exceptions/ # Custom exceptions
│ │ ├── BusinessRuleException.cs
│ │ ├── EntityNotFoundException.cs
│ │ └── ValidationException.cs
│ ├── Services/ # Core services
│ │ ├── IValidationService.cs
│ │ └── ValidationService.cs
│ ├── {{ PrefixName }}{{ SuffixName }}Core.cs
│ └── {{ PrefixName }}{{ SuffixName }}.Core.csproj
├── {{ PrefixName }}{{ SuffixName }}.Persistence/ # Data access
│ ├── Context/
│ │ └── AppDbContext.cs # EF DbContext
│ ├── Entities/ # Database entities
│ │ ├── {{ PrefixName }}Entity.cs
│ │ ├── AbstractCreated.cs
│ │ └── AbstractEntity.cs
│ ├── Migrations/ # EF migrations
│ │ └── 20241024141140_InitialCreation.cs
│ ├── Models/ # Data models
│ │ ├── Page.cs
│ │ └── PageRequest.cs
│ ├── Repositories/ # Repository pattern
│ │ ├── {{ PrefixName }}Repository.cs
│ │ ├── BaseRepository.cs
│ │ └── I{{ PrefixName }}Repository.cs
│ └── {{ PrefixName }}{{ SuffixName }}.Persistence.csproj
├── {{ PrefixName }}{{ SuffixName }}.Server/ # Web server
│ ├── Controllers/ # REST controllers
│ │ ├── {{ PrefixName }}{{ SuffixName }}Controller.cs
│ │ └── AuthController.cs
│ ├── HealthChecks/ # Health endpoints
│ │ ├── DatabaseHealthCheck.cs
│ │ └── ServiceHealthCheck.cs
│ ├── Middleware/ # HTTP middleware
│ │ ├── GlobalExceptionMiddleware.cs
│ │ └── MetricsMiddleware.cs
│ ├── Services/ # Server services
│ │ ├── EphemeralDatabaseService.cs
│ │ ├── JwtAuthenticationService.cs
│ │ └── MetricsService.cs
│ ├── {{ PrefixName }}{{ SuffixName }}Server.cs
│ ├── Startup.cs
│ └── appsettings.json
├── {{ PrefixName }}{{ SuffixName }}.IntegrationTests/ # Integration tests
│ ├── {{ PrefixName }}{{ SuffixName }}RestIT.cs
│ ├── ApplicationFixture.cs
│ └── {{ PrefixName }}{{ SuffixName }}.IntegrationTests.csproj
├── {{ PrefixName }}{{ SuffixName }}.UnitTests/ # Unit tests
│ ├── Core/ # Core logic tests
│ ├── Persistence/ # Repository tests
│ │ └── {{ PrefixName }}RepositoryTests.cs
│ └── {{ PrefixName }}{{ SuffixName }}.UnitTests.csproj
├── {{ PrefixName }}{{ SuffixName }}.sln # Solution file
├── Directory.Build.props # MSBuild properties
├── docker-compose.yml # Local development
├── Dockerfile # Container build
├── NuGet.config # Package sources
├── README.md # Documentation
└── test-endpoints.csx # API testing script

Configuration & Prompts

During archetype generation, you'll be prompted for:

PropertyDescriptionExample
projectService domain name for entities and servicesShopping Cart
suffixUsed with project name for package namingService
group-prefixPackage naming prefixcom.company
team-nameTeam ownership identifierGrowth
service-portHTTP service port8080
management-portHealth check and metrics port8081

REST API Design

Entity Model

// Example entity generated based on project name
public class ShoppingCartItem
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}

Controller Implementation

[ApiController]
[Route("api/[controller]")]
public class ShoppingCartItemsController : ControllerBase
{
private readonly IShoppingCartItemService _service;
private readonly ILogger<ShoppingCartItemsController> _logger;

public ShoppingCartItemsController(
IShoppingCartItemService service,
ILogger<ShoppingCartItemsController> logger)
{
_service = service;
_logger = logger;
}

[HttpGet]
public async Task<ActionResult<IEnumerable<ShoppingCartItemDto>>> GetAll()
{
try
{
var items = await _service.GetAllAsync();
return Ok(items);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving shopping cart items");
return StatusCode(500, "Internal server error");
}
}

[HttpGet("{id}")]
public async Task<ActionResult<ShoppingCartItemDto>> GetById(int id)
{
try
{
var item = await _service.GetByIdAsync(id);
if (item == null)
{
return NotFound($"Shopping cart item with ID {id} not found");
}
return Ok(item);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving shopping cart item {Id}", id);
return StatusCode(500, "Internal server error");
}
}

[HttpPost]
public async Task<ActionResult<ShoppingCartItemDto>> Create(CreateShoppingCartItemDto createDto)
{
try
{
var item = await _service.CreateAsync(createDto);
return CreatedAtAction(nameof(GetById), new { id = item.Id }, item);
}
catch (ValidationException ex)
{
return BadRequest(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating shopping cart item");
return StatusCode(500, "Internal server error");
}
}

[HttpPut("{id}")]
public async Task<ActionResult<ShoppingCartItemDto>> Update(int id, UpdateShoppingCartItemDto updateDto)
{
try
{
var item = await _service.UpdateAsync(id, updateDto);
if (item == null)
{
return NotFound($"Shopping cart item with ID {id} not found");
}
return Ok(item);
}
catch (ValidationException ex)
{
return BadRequest(ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error updating shopping cart item {Id}", id);
return StatusCode(500, "Internal server error");
}
}

[HttpDelete("{id}")]
public async Task<ActionResult> Delete(int id)
{
try
{
var success = await _service.DeleteAsync(id);
if (!success)
{
return NotFound($"Shopping cart item with ID {id} not found");
}
return NoContent();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deleting shopping cart item {Id}", id);
return StatusCode(500, "Internal server error");
}
}
}

Data Transfer Objects

public class ShoppingCartItemDto
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}

public class CreateShoppingCartItemDto
{
[Required]
[StringLength(100)]
public string Name { get; set; }

[StringLength(500)]
public string Description { get; set; }

[Required]
[Range(0.01, double.MaxValue)]
public decimal Price { get; set; }

[Required]
[Range(1, int.MaxValue)]
public int Quantity { get; set; }
}

public class UpdateShoppingCartItemDto
{
[StringLength(100)]
public string Name { get; set; }

[StringLength(500)]
public string Description { get; set; }

[Range(0.01, double.MaxValue)]
public decimal? Price { get; set; }

[Range(1, int.MaxValue)]
public int? Quantity { get; set; }
}

Entity Framework Configuration

DbContext Implementation

public class ServiceDbContext : DbContext
{
public ServiceDbContext(DbContextOptions<ServiceDbContext> options) : base(options)
{
}

public DbSet<ShoppingCartItem> ShoppingCartItems { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);

modelBuilder.Entity<ShoppingCartItem>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Name).IsRequired().HasMaxLength(100);
entity.Property(e => e.Description).HasMaxLength(500);
entity.Property(e => e.Price).HasColumnType("decimal(18,2)");
entity.Property(e => e.CreatedAt).HasDefaultValueSql("CURRENT_TIMESTAMP");
entity.Property(e => e.UpdatedAt).HasDefaultValueSql("CURRENT_TIMESTAMP");
});
}
}

Repository Pattern

public interface IShoppingCartItemRepository
{
Task<IEnumerable<ShoppingCartItem>> GetAllAsync();
Task<ShoppingCartItem> GetByIdAsync(int id);
Task<ShoppingCartItem> CreateAsync(ShoppingCartItem item);
Task<ShoppingCartItem> UpdateAsync(ShoppingCartItem item);
Task<bool> DeleteAsync(int id);
}

public class ShoppingCartItemRepository : IShoppingCartItemRepository
{
private readonly ServiceDbContext _context;

public ShoppingCartItemRepository(ServiceDbContext context)
{
_context = context;
}

public async Task<IEnumerable<ShoppingCartItem>> GetAllAsync()
{
return await _context.ShoppingCartItems.ToListAsync();
}

public async Task<ShoppingCartItem> GetByIdAsync(int id)
{
return await _context.ShoppingCartItems.FindAsync(id);
}

public async Task<ShoppingCartItem> CreateAsync(ShoppingCartItem item)
{
item.CreatedAt = DateTime.UtcNow;
item.UpdatedAt = DateTime.UtcNow;

_context.ShoppingCartItems.Add(item);
await _context.SaveChangesAsync();
return item;
}

public async Task<ShoppingCartItem> UpdateAsync(ShoppingCartItem item)
{
item.UpdatedAt = DateTime.UtcNow;

_context.ShoppingCartItems.Update(item);
await _context.SaveChangesAsync();
return item;
}

public async Task<bool> DeleteAsync(int id)
{
var item = await _context.ShoppingCartItems.FindAsync(id);
if (item == null) return false;

_context.ShoppingCartItems.Remove(item);
await _context.SaveChangesAsync();
return true;
}
}

Testing Strategy

Integration Tests with Testcontainers

public class ShoppingCartItemsControllerTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
private readonly HttpClient _client;
private readonly PostgreSqlContainer _postgres;

public ShoppingCartItemsControllerTests(WebApplicationFactory<Program> factory)
{
_postgres = new PostgreSqlBuilder()
.WithDatabase("testdb")
.WithUsername("testuser")
.WithPassword("testpass")
.Build();

_factory = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.RemoveAll(typeof(DbContextOptions<ServiceDbContext>));
services.AddDbContext<ServiceDbContext>(options =>
{
options.UseNpgsql(_postgres.GetConnectionString());
});
});
});

_client = _factory.CreateClient();
}

[Fact]
public async Task GetShoppingCartItems_ReturnsEmptyList_WhenNoItemsExist()
{
// Arrange
await _postgres.StartAsync();
await SeedDatabase();

// Act
var response = await _client.GetAsync("/api/shoppingcartitems");

// Assert
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
var items = JsonSerializer.Deserialize<List<ShoppingCartItemDto>>(content);
Assert.Empty(items);
}

[Fact]
public async Task CreateShoppingCartItem_ReturnsCreatedItem_WhenValidData()
{
// Arrange
await _postgres.StartAsync();
await SeedDatabase();

var createDto = new CreateShoppingCartItemDto
{
Name = "Test Item",
Description = "Test Description",
Price = 19.99m,
Quantity = 2
};

// Act
var response = await _client.PostAsJsonAsync("/api/shoppingcartitems", createDto);

// Assert
response.EnsureSuccessStatusCode();
Assert.Equal(HttpStatusCode.Created, response.StatusCode);

var content = await response.Content.ReadAsStringAsync();
var item = JsonSerializer.Deserialize<ShoppingCartItemDto>(content);
Assert.Equal(createDto.Name, item.Name);
Assert.Equal(createDto.Price, item.Price);
}
}

Load Testing with k6

import http from 'k6/http';
import { check, sleep } from 'k6';

export let options = {
stages: [
{ duration: '30s', target: 20 },
{ duration: '1m', target: 20 },
{ duration: '30s', target: 0 },
],
};

export default function () {
// Test GET all items
let response = http.get('http://localhost:8080/api/shoppingcartitems');
check(response, {
'GET /api/shoppingcartitems status is 200': (r) => r.status === 200,
'GET response time < 500ms': (r) => r.timings.duration < 500,
});

// Test POST new item
let payload = JSON.stringify({
name: 'Load Test Item',
description: 'Created during load test',
price: 9.99,
quantity: 1
});

let params = {
headers: {
'Content-Type': 'application/json',
},
};

response = http.post('http://localhost:8080/api/shoppingcartitems', payload, params);
check(response, {
'POST /api/shoppingcartitems status is 201': (r) => r.status === 201,
'POST response time < 1000ms': (r) => r.timings.duration < 1000,
});

sleep(1);
}

Deployment

Docker Configuration

# Multi-stage Docker build
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 8080
EXPOSE 8081

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["src/ServiceName.Api/ServiceName.Api.csproj", "src/ServiceName.Api/"]
COPY ["src/ServiceName.Core/ServiceName.Core.csproj", "src/ServiceName.Core/"]
COPY ["src/ServiceName.Infrastructure/ServiceName.Infrastructure.csproj", "src/ServiceName.Infrastructure/"]
RUN dotnet restore "src/ServiceName.Api/ServiceName.Api.csproj"
COPY . .
WORKDIR "/src/src/ServiceName.Api"
RUN dotnet build "ServiceName.Api.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "ServiceName.Api.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ServiceName.Api.dll"]

Kubernetes Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
name: shopping-cart-service
labels:
app: shopping-cart-service
spec:
replicas: 3
selector:
matchLabels:
app: shopping-cart-service
template:
metadata:
labels:
app: shopping-cart-service
spec:
containers:
- name: shopping-cart-service
image: your-registry/shopping-cart-service:latest
ports:
- containerPort: 8080
- containerPort: 8081
env:
- name: ASPNETCORE_ENVIRONMENT
value: "Production"
- name: ConnectionStrings__DefaultConnection
valueFrom:
secretKeyRef:
name: db-secret
key: connection-string
livenessProbe:
httpGet:
path: /health
port: 8081
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health/ready
port: 8081
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: shopping-cart-service
spec:
selector:
app: shopping-cart-service
ports:
- name: http
port: 80
targetPort: 8080
- name: management
port: 8081
targetPort: 8081

Quick Start

  1. Generate the service:

    archetect render git@github.com:p6m-archetypes/dotnet-rest-service-basic.archetype.git
  2. Run locally with Docker Compose:

    docker-compose up -d
  3. Apply database migrations:

    dotnet ef database update
  4. Start the service:

    dotnet run --project src/ServiceName.Api
  5. Access the API:

  6. Run tests:

    dotnet test

Best Practices

API Design

  • Use proper HTTP status codes
  • Implement consistent error responses
  • Version your APIs appropriately
  • Document all endpoints with OpenAPI

Performance

  • Use async/await consistently
  • Implement caching where appropriate
  • Optimize database queries
  • Monitor application metrics

Security

  • Validate all inputs
  • Use HTTPS in production
  • Implement proper authentication
  • Follow OWASP guidelines

Testing

  • Maintain high test coverage
  • Use integration tests for critical paths
  • Implement performance testing
  • Test error scenarios