REST API in ConnectSoft Microservice Template¶
Purpose & Overview¶
The REST API implementation in the ConnectSoft Microservice Template provides a standardized HTTP-based interface for accessing microservice functionality. It follows RESTful principles and integrates seamlessly with the template's Clean Architecture, CQRS pattern, and domain-driven design approach.
Why REST API?¶
REST API offers several benefits for microservices:
- Universal Compatibility: Works with any HTTP client, including web browsers, mobile apps, and other services
- Statelessness: Each request contains all information needed to process it, enabling scalability
- Cacheability: HTTP caching can significantly improve performance
- Standard HTTP Methods: Leverages well-understood HTTP verbs (GET, POST, PUT, DELETE, PATCH)
- Discoverability: OpenAPI/Swagger documentation provides self-documenting APIs
- Platform Independence: Works across different programming languages and platforms
Architecture Overview¶
REST API sits at the outermost layer of the Clean Architecture:
HTTP Client
↓
REST API Controllers (ServiceModel.RestApi)
├── Request DTOs (ServiceModel)
├── Response DTOs (ServiceModel)
└── AutoMapper (Request/Response ↔ Domain Input/Output)
↓
Domain Model (DomainModel)
├── Processors (Commands/Writes)
└── Retrievers (Queries/Reads)
↓
Repository Layer (PersistenceModel)
└── Data Store
Core Components¶
1. Controllers¶
Controllers handle HTTP requests and delegate to domain services:
[ApiController]
[Route("api/[controller]")]
public class MicroserviceAggregateRootsServiceController(
ILogger<MicroserviceAggregateRootsServiceController> logger,
IMicroserviceAggregateRootsRetriever retriever,
IMicroserviceAggregateRootsProcessor processor,
IMapper mapper)
: ControllerBase, IMicroserviceAggregateRootQueryService, IMicroserviceAggregateRootProcessService
{
private readonly ILogger<MicroserviceAggregateRootsServiceController> logger = logger;
private readonly IMicroserviceAggregateRootsRetriever retriever = retriever;
private readonly IMicroserviceAggregateRootsProcessor processor = processor;
private readonly IMapper mapper = mapper;
/// <summary>
/// Process, create and store a MicroserviceAggregateRoots.
/// </summary>
[HttpPost("MicroserviceAggregateRoots/", Name = nameof(CreateMicroserviceAggregateRoot))]
[SwaggerOperation(
Summary = "Process, create and store a MicroserviceAggregateRoots.",
Description = "Process, create and store a MicroserviceAggregateRoots.",
OperationId = nameof(CreateMicroserviceAggregateRoot),
Tags = ["MicroserviceAggregateRoots"])]
[SwaggerResponse(StatusCodes.Status200OK, "Created MicroserviceAggregateRoot.", typeof(CreateMicroserviceAggregateRootResponse), MediaTypeNames.Application.Json)]
[SwaggerResponse(StatusCodes.Status400BadRequest, "The request is invalid.", typeof(ValidationProblemDetails), MediaTypeNames.Application.ProblemJson)]
[SwaggerResponse(StatusCodes.Status500InternalServerError, "An un-handled exception occurred.", typeof(ProblemDetails), MediaTypeNames.Application.ProblemJson)]
public async Task<CreateMicroserviceAggregateRootResponse> CreateMicroserviceAggregateRoot(
[FromBody, SwaggerRequestBody("Create MicroserviceAggregateRoot request payload", Required = true)]
CreateMicroserviceAggregateRootRequest request,
CancellationToken token = default)
{
Guid objectId = request.ObjectId;
this.logger.Here(log => log.LogInformation("Create MicroserviceAggregateRoot for {ObjectId} started...", objectId));
CreateMicroserviceAggregateRootInput input = this.mapper.Map<CreateMicroserviceAggregateRootRequest, CreateMicroserviceAggregateRootInput>(request);
IMicroserviceAggregateRoot foundEntity = await this.processor.CreateMicroserviceAggregateRoot(input, token).ConfigureAwait(false);
CreateMicroserviceAggregateRootResponse response = null;
if (foundEntity != null)
{
response = new CreateMicroserviceAggregateRootResponse
{
CreatedMicroserviceAggregateRoot = this.mapper.Map<IMicroserviceAggregateRoot, MicroserviceAggregateRootDto>(foundEntity),
};
}
this.logger.Here(log => log.LogInformation("Create MicroserviceAggregateRoot for {ObjectId} finished...", objectId));
return await Task.FromResult(response).ConfigureAwait(false);
}
/// <summary>
/// Gets MicroserviceAggregateRoot details.
/// </summary>
[HttpGet("MicroserviceAggregateRoots/", Name = nameof(GetMicroserviceAggregateRootDetails))]
[SwaggerOperation(
Summary = "Gets MicroserviceAggregateRoot details.",
Description = "Gets MicroserviceAggregateRoot details.",
OperationId = nameof(GetMicroserviceAggregateRootDetails),
Tags = ["MicroserviceAggregateRoots"])]
[SwaggerResponse(StatusCodes.Status200OK, "Gets MicroserviceAggregateRoot details.", typeof(GetMicroserviceAggregateRootDetailsResponse), MediaTypeNames.Application.Json)]
[SwaggerResponse(StatusCodes.Status404NotFound, "A MicroserviceAggregateRoot with the specified identifier not found.", typeof(ProblemDetails), MediaTypeNames.Application.ProblemJson)]
public async Task<GetMicroserviceAggregateRootDetailsResponse> GetMicroserviceAggregateRootDetails(
[FromQuery] GetMicroserviceAggregateRootDetailsRequest request,
CancellationToken token = default)
{
Guid objectId = request.ObjectId;
this.logger.Here(log => log.LogInformation("Get MicroserviceAggregateRoot details for {ObjectId} started...", objectId));
GetMicroserviceAggregateRootDetailsInput input = this.mapper.Map<GetMicroserviceAggregateRootDetailsRequest, GetMicroserviceAggregateRootDetailsInput>(request);
IMicroserviceAggregateRoot foundEntity = await this.retriever.GetMicroserviceAggregateRootDetails(input, token).ConfigureAwait(false);
GetMicroserviceAggregateRootDetailsResponse response = null;
if (foundEntity != null)
{
response = new GetMicroserviceAggregateRootDetailsResponse
{
FoundMicroserviceAggregateRoot = this.mapper.Map<IMicroserviceAggregateRoot, MicroserviceAggregateRootDto>(foundEntity),
};
}
this.logger.Here(log => log.LogInformation("Get MicroserviceAggregateRoot details for {ObjectId} finished...", objectId));
return await Task.FromResult(response).ConfigureAwait(false);
}
/// <summary>
/// Delete a given MicroserviceAggregateRoot.
/// </summary>
[HttpDelete("MicroserviceAggregateRoots/", Name = nameof(DeleteMicroserviceAggregateRoot))]
[SwaggerOperation(
Summary = "Delete a given MicroserviceAggregateRoot.",
Description = "Delete a given MicroserviceAggregateRoot.",
OperationId = nameof(DeleteMicroserviceAggregateRoot),
Tags = ["MicroserviceAggregateRoots"])]
[SwaggerResponse(StatusCodes.Status200OK, "The MicroserviceAggregateRoot successfully deleted.")]
[SwaggerResponse(StatusCodes.Status404NotFound, "A MicroserviceAggregateRoot with the specified identifier not found.", typeof(ProblemDetails), MediaTypeNames.Application.ProblemJson)]
public async Task DeleteMicroserviceAggregateRoot(
[FromBody, SwaggerRequestBody("Delete MicroserviceAggregateRoot request payload", Required = true)]
DeleteMicroserviceAggregateRootRequest request,
CancellationToken token = default)
{
Guid objectId = request.ObjectId;
this.logger.Here(log => log.LogInformation("Delete MicroserviceAggregateRoot for {ObjectId} started...", objectId));
DeleteMicroserviceAggregateRootInput input = this.mapper.Map<DeleteMicroserviceAggregateRootRequest, DeleteMicroserviceAggregateRootInput>(request);
await this.processor.DeleteMicroserviceAggregateRoot(input, token).ConfigureAwait(false);
this.logger.Here(log => log.LogInformation("Delete MicroserviceAggregateRoot for {ObjectId} finished...", objectId));
await Task.CompletedTask.ConfigureAwait(false);
}
}
2. Request/Response DTOs¶
Request and Response objects are defined in the ServiceModel project:
// Request DTO
public class CreateMicroserviceAggregateRootRequest
{
[Required]
[NotDefault]
public Guid ObjectId { get; set; }
// Additional request properties
}
// Response DTO
public class CreateMicroserviceAggregateRootResponse
{
public MicroserviceAggregateRootDto CreatedMicroserviceAggregateRoot { get; set; }
}
3. HTTP Status Codes¶
The template uses standard HTTP status codes:
| Status Code | Meaning | Usage |
|---|---|---|
| 200 OK | Success | Successful GET, PUT, DELETE, PATCH |
| 201 Created | Resource created | Successful POST that creates a resource |
| 400 Bad Request | Invalid request | Validation errors, malformed request |
| 401 Unauthorized | Authentication required | Missing or invalid authentication |
| 403 Forbidden | Access denied | Authenticated but not authorized |
| 404 Not Found | Resource not found | Requested resource doesn't exist |
| 409 Conflict | Conflict | Resource conflict (e.g., duplicate) |
| 422 Unprocessable Entity | Business logic error | Valid syntax but business rule violation |
| 500 Internal Server Error | Server error | Unexpected server error |
| 502 Bad Gateway | Gateway error | Invalid response from upstream |
| 503 Service Unavailable | Service unavailable | Service temporarily unavailable |
| 504 Gateway Timeout | Timeout | Upstream service timeout |
4. Swagger/OpenAPI Integration¶
The template includes comprehensive Swagger/OpenAPI documentation:
// SwaggerExtensions.cs
internal static IServiceCollection AddSwagger(this IServiceCollection services)
{
if (OptionsExtensions.SwaggerOptions.EnableSwagger)
{
string serviceModelXmlCommentsFilePath = null, apiModelXmlCommentsFilePath = null;
// Find XML documentation files
string[] files = Directory.GetFiles(
AppDomain.CurrentDomain.BaseDirectory,
searchPattern: "ConnectSoft.MicroserviceTemplate.ServiceModel.xml",
searchOption: SearchOption.AllDirectories);
if (files != null && files.Length == 1)
{
serviceModelXmlCommentsFilePath = files[0];
}
files = Directory.GetFiles(
AppDomain.CurrentDomain.BaseDirectory,
searchPattern: "ConnectSoft.MicroserviceTemplate.ServiceModel.RestApi.xml",
searchOption: SearchOption.AllDirectories);
if (files != null && files.Length == 1)
{
apiModelXmlCommentsFilePath = files[0];
}
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "ConnectSoft.MicroserviceTemplate's microservice back-end API",
Version = "v1"
});
if (!string.IsNullOrEmpty(serviceModelXmlCommentsFilePath))
{
c.IncludeXmlComments(serviceModelXmlCommentsFilePath);
}
if (!string.IsNullOrEmpty(apiModelXmlCommentsFilePath))
{
c.IncludeXmlComments(apiModelXmlCommentsFilePath);
}
c.EnableAnnotations(enableAnnotationsForInheritance: true, enableAnnotationsForPolymorphism: true);
c.UseAllOfForInheritance();
});
}
return services;
}
internal static IApplicationBuilder UseMicroserviceSwagger(this IApplicationBuilder application)
{
SwaggerOptions swaggerOptions = application.ApplicationServices
.GetRequiredService<IOptions<SwaggerOptions>>().Value;
if (swaggerOptions.EnableSwagger)
{
// Enable middleware to serve generated Swagger as a JSON endpoint
application.UseSwagger();
if (swaggerOptions.EnableSwaggerUI)
{
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.)
application.UseSwaggerUI(options =>
{
options.DocumentTitle = "ConnectSoft.MicroserviceTemplate's microservice back-end API";
if (!string.IsNullOrEmpty(swaggerOptions.SwaggerUITheme))
{
options.InjectStylesheet(swaggerOptions.SwaggerUITheme);
}
options.SwaggerEndpoint("/swagger/v1/swagger.json",
"ConnectSoft.MicroserviceTemplate's microservice back-end API.");
options.DisplayOperationId();
options.DisplayRequestDuration();
});
}
}
return application;
}
5. Controller Registration¶
Controllers are registered via application parts:
// WebApiExtensions.cs
internal static IServiceCollection AddWebApiCommunication(this IServiceCollection services)
{
services
.AddControllers(config =>
{
config.ReturnHttpNotAcceptable = true;
config.RespectBrowserAcceptHeader = true;
})
.AddJsonOptions(jsonOptions =>
{
jsonOptions.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
})
.AddDataAnnotationsLocalization()
.AddApplicationPart(typeof(MicroserviceAggregateRootsServiceController).Assembly)
.AddControllersAsServices(); // Register controllers in DI container
return services;
}
internal static IEndpointRouteBuilder MapMicroserviceControllers(this IEndpointRouteBuilder endpoints)
{
endpoints.MapControllers();
return endpoints;
}
REST API Best Practices¶
Do's¶
- Use appropriate HTTP methods
- GET: Retrieve resources (idempotent)
- POST: Create resources or perform actions
- PUT: Update entire resource (idempotent)
- PATCH: Partial update (idempotent)
-
DELETE: Remove resources (idempotent)
-
Follow RESTful naming conventions
- Use nouns for resources:
/api/MicroserviceAggregateRoots - Use plural nouns:
/api/Usersnot/api/User -
Avoid verbs in URLs:
/api/GetUser❌ →/api/Users/{id}✅ -
Use appropriate status codes
- 200 OK for successful GET, PUT, DELETE
- 201 Created for successful POST that creates resources
- 400 Bad Request for validation errors
- 404 Not Found for missing resources
-
422 Unprocessable Entity for business logic violations
-
Provide comprehensive Swagger documentation
- Document all endpoints with
[SwaggerOperation] - Specify response types with
[SwaggerResponse] -
Include XML comments for detailed descriptions
-
Handle errors consistently
- Use
ValidationProblemDetailsfor validation errors - Use
ProblemDetailsfor other errors -
Include correlation IDs for error tracking
-
Support async operations
- All controller actions should be async
- Use
CancellationTokenfor cancellation support -
Configure await false when appropriate
-
Map between layers
- Use AutoMapper to map between Request/Response DTOs and Domain Input/Output
- Never expose domain entities directly
- Keep DTOs focused on API concerns
Don'ts¶
- Don't put business logic in controllers
- Controllers should only handle HTTP concerns
-
Delegate all business logic to domain services
-
Don't expose domain entities directly
- Use DTOs to isolate API from domain model
-
Prevent internal structure leakage
-
Don't use GET for mutations
- GET requests should be idempotent and side-effect free
-
Use POST for actions that modify state
-
Don't ignore HTTP semantics
- Respect HTTP method semantics
-
Use appropriate status codes
-
Don't mix concerns
- Keep controllers thin
- Handle HTTP concerns only
Integration with CQRS¶
REST API integrates with CQRS pattern:
// Command endpoint (write operation)
[HttpPost("MicroserviceAggregateRoots/")]
public async Task<CreateMicroserviceAggregateRootResponse> CreateMicroserviceAggregateRoot(
[FromBody] CreateMicroserviceAggregateRootRequest request,
CancellationToken token = default)
{
// Map request to domain input
var input = mapper.Map<CreateMicroserviceAggregateRootInput>(request);
// Use Processor (command side)
var result = await processor.CreateMicroserviceAggregateRoot(input, token);
// Map domain output to response
return mapper.Map<CreateMicroserviceAggregateRootResponse>(result);
}
// Query endpoint (read operation)
[HttpGet("MicroserviceAggregateRoots/")]
public async Task<GetMicroserviceAggregateRootDetailsResponse> GetMicroserviceAggregateRootDetails(
[FromQuery] GetMicroserviceAggregateRootDetailsRequest request,
CancellationToken token = default)
{
// Map request to domain input
var input = mapper.Map<GetMicroserviceAggregateRootDetailsInput>(request);
// Use Retriever (query side)
var result = await retriever.GetMicroserviceAggregateRootDetails(input, token);
// Map domain output to response
return mapper.Map<GetMicroserviceAggregateRootDetailsResponse>(result);
}
Error Handling¶
Validation Errors¶
Validation errors are automatically handled by ASP.NET Core:
[HttpPost("MicroserviceAggregateRoots/")]
public async Task<CreateMicroserviceAggregateRootResponse> CreateMicroserviceAggregateRoot(
[FromBody] CreateMicroserviceAggregateRootRequest request,
CancellationToken token = default)
{
// If request is invalid, framework automatically returns 400 Bad Request
// with ValidationProblemDetails containing error details
// Process valid request...
}
Business Logic Errors¶
Domain exceptions should be caught and converted to appropriate HTTP responses:
// In exception handling middleware or controller
try
{
var result = await processor.CreateMicroserviceAggregateRoot(input, token);
return Ok(result);
}
catch (AggregateNotFoundException ex)
{
return NotFound(new ProblemDetails
{
Title = "Resource not found",
Detail = ex.Message,
Status = StatusCodes.Status404NotFound
});
}
catch (BusinessRuleViolationException ex)
{
return UnprocessableEntity(new ValidationProblemDetails
{
Title = "Business rule violation",
Detail = ex.Message,
Status = StatusCodes.Status422UnprocessableEntity
});
}
Configuration¶
Swagger Options¶
Swagger can be configured via SwaggerOptions:
{
"SwaggerOptions": {
"EnableSwagger": true,
"EnableSwaggerUI": true,
"SwaggerUITheme": "/swagger-ui/themes/3.x/theme-monokai.css"
}
}
JSON Serialization¶
JSON options are configured in WebApiExtensions:
.AddJsonOptions(jsonOptions =>
{
// Support enum as strings
jsonOptions.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
// Additional JSON options can be configured here
})
Common Scenarios¶
Scenario 1: Simple CRUD Operations¶
// Create
[HttpPost("Resources/")]
public async Task<CreateResourceResponse> CreateResource([FromBody] CreateResourceRequest request)
{
var input = mapper.Map<CreateResourceInput>(request);
var result = await processor.CreateResource(input);
return mapper.Map<CreateResourceResponse>(result);
}
// Read
[HttpGet("Resources/{id}")]
public async Task<GetResourceResponse> GetResource(Guid id)
{
var input = new GetResourceInput { Id = id };
var result = await retriever.GetResource(input);
return mapper.Map<GetResourceResponse>(result);
}
// Update
[HttpPut("Resources/")]
public async Task<UpdateResourceResponse> UpdateResource([FromBody] UpdateResourceRequest request)
{
var input = mapper.Map<UpdateResourceInput>(request);
var result = await processor.UpdateResource(input);
return mapper.Map<UpdateResourceResponse>(result);
}
// Delete
[HttpDelete("Resources/")]
public async Task DeleteResource([FromBody] DeleteResourceRequest request)
{
var input = mapper.Map<DeleteResourceInput>(request);
await processor.DeleteResource(input);
}
Scenario 2: Query with Filters¶
[HttpGet("Resources/")]
public async Task<SearchResourcesResponse> SearchResources(
[FromQuery] SearchResourcesRequest request)
{
var input = mapper.Map<SearchResourcesInput>(request);
var results = await retriever.SearchResources(input);
return mapper.Map<SearchResourcesResponse>(results);
}
// Request with query parameters
public class SearchResourcesRequest
{
public string? Name { get; set; }
public ResourceStatus? Status { get; set; }
public DateTimeOffset? CreatedAfter { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 20;
}
Scenario 3: Action Endpoints¶
For operations that don't fit standard CRUD:
[HttpPost("Orders/{id}/cancel")]
public async Task<CancelOrderResponse> CancelOrder(
Guid id,
[FromBody] CancelOrderRequest request)
{
var input = new CancelOrderInput
{
OrderId = id,
Reason = request.Reason
};
var result = await processor.CancelOrder(input);
return mapper.Map<CancelOrderResponse>(result);
}
Testing REST API¶
Unit Testing Controllers¶
[Fact]
public async Task CreateMicroserviceAggregateRoot_Should_Return_Ok()
{
// Arrange
var mockProcessor = new Mock<IMicroserviceAggregateRootsProcessor>();
var mockMapper = new Mock<IMapper>();
var controller = new MicroserviceAggregateRootsServiceController(
Mock.Of<ILogger<MicroserviceAggregateRootsServiceController>>(),
Mock.Of<IMicroserviceAggregateRootsRetriever>(),
mockProcessor.Object,
mockMapper.Object);
var request = new CreateMicroserviceAggregateRootRequest { ObjectId = Guid.NewGuid() };
var input = new CreateMicroserviceAggregateRootInput { ObjectId = request.ObjectId };
var entity = Mock.Of<IMicroserviceAggregateRoot>();
var response = new CreateMicroserviceAggregateRootResponse();
mockMapper.Setup(m => m.Map<CreateMicroserviceAggregateRootInput>(request))
.Returns(input);
mockProcessor.Setup(p => p.CreateMicroserviceAggregateRoot(input, It.IsAny<CancellationToken>()))
.ReturnsAsync(entity);
mockMapper.Setup(m => m.Map<CreateMicroserviceAggregateRootResponse>(It.IsAny<object>()))
.Returns(response);
// Act
var result = await controller.CreateMicroserviceAggregateRoot(request);
// Assert
Assert.NotNull(result);
mockProcessor.Verify(p => p.CreateMicroserviceAggregateRoot(input, It.IsAny<CancellationToken>()), Times.Once);
}
Integration Testing¶
Use WebApplicationFactory for integration tests:
public class MicroserviceAggregateRootsControllerTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
private readonly HttpClient _client;
public MicroserviceAggregateRootsControllerTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
_client = _factory.CreateClient();
}
[Fact]
public async Task GetMicroserviceAggregateRoot_Should_Return_Ok()
{
// Arrange
var id = Guid.NewGuid();
// Act
var response = await _client.GetAsync($"/api/MicroserviceAggregateRootsService?ObjectId={id}");
// Assert
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
Assert.NotNull(content);
}
}
Advanced Topics¶
API Versioning¶
API versioning can be implemented using Microsoft.AspNetCore.Mvc.Versioning. See API Versioning for comprehensive versioning strategies, implementation patterns, and best practices across REST API, GraphQL, gRPC, and CoreWCF.
Rate Limiting¶
Rate limiting can be configured globally or per endpoint:
services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: context.User.Identity?.Name ?? context.Request.Headers.Host.ToString(),
factory: partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1)
}));
});
CORS Configuration¶
CORS can be configured for cross-origin requests:
services.AddCors(options =>
{
options.AddPolicy("AllowSpecificOrigin", policy =>
{
policy.WithOrigins("https://example.com")
.AllowAnyMethod()
.AllowAnyHeader();
});
});
Summary¶
The REST API implementation in the ConnectSoft Microservice Template provides:
- ✅ Clean Architecture: Controllers in the outermost layer
- ✅ CQRS Integration: Separate endpoints for commands and queries
- ✅ Comprehensive Documentation: Swagger/OpenAPI integration
- ✅ Error Handling: Standard HTTP status codes and error responses
- ✅ Validation: Automatic request validation
- ✅ Async Support: Full async/await support
- ✅ Testability: Easy to unit and integration test
By following these patterns and best practices, you can build robust, scalable, and maintainable REST APIs that integrate seamlessly with the template's architectural patterns.