.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:
| Property | Description | Example |
|---|---|---|
project | Service domain name for entities and services | Shopping Cart |
suffix | Used with project name for package naming | Service |
group-prefix | Package naming prefix | com.company |
team-name | Team ownership identifier | Growth |
service-port | HTTP service port | 8080 |
management-port | Health check and metrics port | 8081 |
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
-
Generate the service:
archetect render git@github.com:p6m-archetypes/dotnet-rest-service-basic.archetype.git -
Run locally with Docker Compose:
docker-compose up -d -
Apply database migrations:
dotnet ef database update -
Start the service:
dotnet run --project src/ServiceName.Api -
Access the API:
- Swagger UI: http://localhost:8080/swagger
- Health checks: http://localhost:8081/health
- Metrics: http://localhost:8081/metrics
-
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