Documentation
reference/api/api-documentation.md
API Documentation Guide for Dynaplex Architecture
This guide covers API design, documentation, and client generation standards for the Acsis Core Dynaplex architecture.
๐ฏ API Design Principles
The Dynaplex architecture follows these API design principles:
- RESTful Design: Follow REST conventions for resource-based APIs
- Contract-First: Define OpenAPI specifications before implementation
- Type Safety: Generate strongly-typed clients from OpenAPI specs
- Versioning: Support multiple API versions gracefully
- Consistency: Maintain consistent patterns across all services
๐ OpenAPI Specification
OpenAPI Structure
Each service must maintain its OpenAPI specification:
components/your-component/
โโโ src/
โ โโโ Acsis.Components.YourComponent/
โ โโโ openapi.yaml # OpenAPI 3.0+ specification
โ โโโ Controllers/ # Implementation matching spec
OpenAPI Template
openapi: 3.0.3
info:
title: Asset Service API
description: Dynaplex Asset Management Service
version: 1.0.0
contact:
name: API Support
email: api-support@acsis.com
servers:
- url: https://localhost:40443
description: Local development
- url: https://api.acsis.com
description: Production
tags:
- name: Assets
description: Asset management operations
- name: Health
description: Service health endpoints
paths:
/api/v1/assets:
get:
tags:
- Assets
summary: Get all assets
operationId: getAssets
parameters:
- $ref: '#/components/parameters/PageNumber'
- $ref: '#/components/parameters/PageSize'
- $ref: '#/components/parameters/SortBy'
- $ref: '#/components/parameters/FilterBy'
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/AssetListResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'500':
$ref: '#/components/responses/InternalServerError'
post:
tags:
- Assets
summary: Create a new asset
operationId: createAsset
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateAssetRequest'
responses:
'201':
description: Asset created
content:
application/json:
schema:
$ref: '#/components/schemas/Asset'
headers:
Location:
description: URL of the created asset
schema:
type: string
'400':
$ref: '#/components/responses/BadRequest'
'409':
$ref: '#/components/responses/Conflict'
/api/v1/assets/{assetId}:
get:
tags:
- Assets
summary: Get asset by ID
operationId: getAssetById
parameters:
- name: assetId
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/Asset'
'404':
$ref: '#/components/responses/NotFound'
components:
schemas:
Asset:
type: object
required:
- id
- name
- type
- status
properties:
id:
type: string
format: uuid
description: Unique identifier
name:
type: string
minLength: 1
maxLength: 255
description: Asset name
type:
type: string
enum: [Equipment, Vehicle, Tool, Facility]
description: Asset type
status:
type: string
enum: [Active, Inactive, Maintenance, Retired]
description: Current status
location:
$ref: '#/components/schemas/Location'
attributes:
type: object
additionalProperties: true
description: Custom attributes
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
CreateAssetRequest:
type: object
required:
- name
- type
properties:
name:
type: string
minLength: 1
maxLength: 255
type:
type: string
enum: [Equipment, Vehicle, Tool, Facility]
locationId:
type: string
format: uuid
attributes:
type: object
additionalProperties: true
AssetListResponse:
type: object
properties:
items:
type: array
items:
$ref: '#/components/schemas/Asset'
pagination:
$ref: '#/components/schemas/PaginationInfo'
PaginationInfo:
type: object
properties:
currentPage:
type: integer
minimum: 1
pageSize:
type: integer
minimum: 1
maximum: 100
totalItems:
type: integer
totalPages:
type: integer
Error:
type: object
required:
- code
- message
properties:
code:
type: string
message:
type: string
details:
type: array
items:
type: object
properties:
field:
type: string
message:
type: string
Location:
type: object
properties:
id:
type: string
format: uuid
name:
type: string
path:
type: string
description: Hierarchical path
parameters:
PageNumber:
name: page
in: query
schema:
type: integer
minimum: 1
default: 1
PageSize:
name: pageSize
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
SortBy:
name: sortBy
in: query
schema:
type: string
enum: [name, createdAt, updatedAt]
default: createdAt
FilterBy:
name: filter
in: query
schema:
type: string
description: Filter expression
responses:
BadRequest:
description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Unauthorized:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Conflict:
description: Resource conflict
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
InternalServerError:
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
apiKey:
type: apiKey
in: header
name: X-API-Key
security:
- bearerAuth: []
- apiKey: []
๐ Scalar API Documentation
Configuring Scalar
Each Dynaplex service includes Scalar for interactive API documentation:
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add services
builder.Services.AddControllers();
builder.Services.AddOpenApi();
var app = builder.Build();
// Configure Scalar
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference(options =>
{
options.Title = "Asset Service API";
options.Theme = ScalarTheme.Modern;
options.DarkMode = true;
options.ShowSidebar = true;
options.DefaultOpenAllTags = true;
options.CustomCss = @"
.scalar-card { border-radius: 8px; }
.scalar-button { border-radius: 4px; }
";
});
}
Accessing Scalar Documentation
Each service exposes Scalar at /scalar:
- CoreData: https://localhost:40443/scalar
- Catalog: https://localhost:43443/scalar
- Identity: https://localhost:41443/scalar
Scalar Features
- Interactive Testing: Execute API calls directly from the documentation
- Authentication: Configure auth tokens for testing protected endpoints
- Request Builder: Construct complex requests with headers and parameters
- Response Viewer: Inspect response headers, body, and status codes
- Code Generation: Generate client code in multiple languages
๐ง Microsoft Kiota Client Generation
Installing Kiota
# Install Kiota CLI
dotnet tool install -g Microsoft.OpenApi.Kiota
# Verify installation
kiota --version
Generating API Clients
Manual Generation
# Generate C# client from OpenAPI spec
kiota generate \
--language csharp \
--openapi ./openapi.yaml \
--output ./ApiClient \
--namespace Acsis.Components.Asset.ApiClient \
--class-name AssetApiClient
Automated Generation in Build
Add to .csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Kiota.Abstractions" Version="1.0.0" />
<PackageReference Include="Microsoft.Kiota.Http.HttpClientLibrary" Version="1.0.0" />
<PackageReference Include="Microsoft.Kiota.Serialization.Json" Version="1.0.0" />
</ItemGroup>
<Target Name="GenerateApiClient" BeforeTargets="BeforeBuild">
<Exec Command="kiota generate --language csharp --openapi ../Acsis.Components.Asset/openapi.yaml --output ./Generated --namespace $(RootNamespace)" />
</Target>
</Project>
Using Generated Clients
// Configure DI
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddAssetApiClient(
this IServiceCollection services,
string baseUrl)
{
services.AddHttpClient<AssetApiClient>(client =>
{
client.BaseAddress = new Uri(baseUrl);
});
services.AddTransient<IAuthenticationProvider, BearerTokenAuthenticationProvider>();
services.AddTransient<IRequestAdapter, HttpClientRequestAdapter>();
services.AddTransient(sp =>
{
var httpClient = sp.GetRequiredService<HttpClient>();
var authProvider = sp.GetRequiredService<IAuthenticationProvider>();
var requestAdapter = new HttpClientRequestAdapter(authProvider, httpClient);
return new AssetApiClient(requestAdapter);
});
return services;
}
}
// Usage in service
public class CatalogService
{
private readonly AssetApiClient _assetClient;
public CatalogService(AssetApiClient assetClient)
{
_assetClient = assetClient;
}
public async Task<Asset> GetAssetAsync(Guid id)
{
return await _assetClient.Api.V1.Assets[id.ToString()].GetAsync();
}
public async Task<AssetListResponse> GetAssetsAsync(int page = 1, int pageSize = 20)
{
return await _assetClient.Api.V1.Assets.GetAsync(config =>
{
config.QueryParameters.Page = page;
config.QueryParameters.PageSize = pageSize;
});
}
public async Task<Asset> CreateAssetAsync(CreateAssetRequest request)
{
return await _assetClient.Api.V1.Assets.PostAsync(request);
}
}
๐ API Versioning
Versioning Strategy
// Program.cs
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
options.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(),
new HeaderApiVersionReader("X-API-Version"),
new MediaTypeApiVersionReader("version")
);
});
builder.Services.AddVersionedApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
Versioned Controllers
[ApiController]
[Route("api/v{version:apiVersion}/assets")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class AssetController : ControllerBase
{
[HttpGet]
[MapToApiVersion("1.0")]
public async Task<ActionResult<IEnumerable<AssetV1Dto>>> GetV1()
{
// V1 implementation
}
[HttpGet]
[MapToApiVersion("2.0")]
public async Task<ActionResult<IEnumerable<AssetV2Dto>>> GetV2()
{
// V2 implementation with additional fields
}
}
๐ API Design Standards
Resource Naming
# Good - Plural nouns for collections
GET /api/v1/assets
GET /api/v1/locations
GET /api/v1/users
# Good - Nested resources
GET /api/v1/assets/{id}/attributes
GET /api/v1/locations/{id}/children
# Bad - Verbs in URLs
GET /api/v1/getAssets
POST /api/v1/createAsset
HTTP Methods
| Method | Usage | Idempotent | Safe |
|---|---|---|---|
| GET | Retrieve resource(s) | Yes | Yes |
| POST | Create new resource | No | No |
| PUT | Replace entire resource | Yes | No |
| PATCH | Partial update | No | No |
| DELETE | Remove resource | Yes | No |
Status Codes
// Success codes
200 OK // Successful GET, PUT, PATCH
201 Created // Successful POST
204 No Content // Successful DELETE
202 Accepted // Async operation started
// Client error codes
400 Bad Request // Invalid request data
401 Unauthorized // Missing/invalid auth
403 Forbidden // Authorized but not allowed
404 Not Found // Resource doesn't exist
409 Conflict // Resource state conflict
422 Unprocessable // Validation errors
// Server error codes
500 Internal Error // Unexpected server error
502 Bad Gateway // Upstream service error
503 Unavailable // Service temporarily down
504 Gateway Timeout // Upstream timeout
Request/Response Format
Standard Response Envelope
{
"data": { /* Resource data */ },
"metadata": {
"timestamp": "2024-01-01T00:00:00Z",
"version": "1.0",
"requestId": "abc-123"
},
"pagination": {
"page": 1,
"pageSize": 20,
"totalItems": 100,
"totalPages": 5
}
}
Error Response Format
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"timestamp": "2024-01-01T00:00:00Z",
"requestId": "abc-123",
"details": [
{
"field": "name",
"message": "Name is required"
},
{
"field": "email",
"message": "Invalid email format"
}
]
}
}
Pagination
public class PaginatedRequest
{
[FromQuery] public int Page { get; set; } = 1;
[FromQuery] public int PageSize { get; set; } = 20;
[FromQuery] public string SortBy { get; set; } = "createdAt";
[FromQuery] public SortDirection SortDirection { get; set; } = SortDirection.Desc;
}
public class PaginatedResponse<T>
{
public IEnumerable<T> Items { get; set; }
public int CurrentPage { get; set; }
public int PageSize { get; set; }
public int TotalItems { get; set; }
public int TotalPages { get; set; }
public bool HasPrevious => CurrentPage > 1;
public bool HasNext => CurrentPage < TotalPages;
}
Filtering and Searching
# Simple filtering
GET /api/v1/assets?status=active&type=equipment
# Range queries
GET /api/v1/assets?createdAfter=2024-01-01&createdBefore=2024-12-31
# Search
GET /api/v1/assets?q=pump&fields=name,description
# Complex filtering (OData-style)
GET /api/v1/assets?$filter=status eq 'active' and price gt 1000
๐ API Security
Authentication
// JWT Bearer configuration
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = configuration["Auth:Authority"];
options.Audience = configuration["Auth:Audience"];
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true
};
});
// API Key authentication
public class ApiKeyAuthenticationHandler : AuthenticationHandler<ApiKeyAuthenticationOptions>
{
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.TryGetValue("X-API-Key", out var apiKey))
{
return AuthenticateResult.Fail("Missing API Key");
}
// Validate API key
var isValid = await ValidateApiKeyAsync(apiKey);
if (!isValid)
{
return AuthenticateResult.Fail("Invalid API Key");
}
var claims = new[] { new Claim(ClaimTypes.Name, "ApiKeyUser") };
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
Rate Limiting
// Configure rate limiting
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(
httpContext => RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.User?.Identity?.Name ?? httpContext.Request.Headers.Host.ToString(),
factory: partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1)
}));
});
// Apply to controllers
[ApiController]
[EnableRateLimiting("api")]
public class AssetController : ControllerBase
{
// Endpoints are rate limited
}
๐ API Monitoring
Health Checks
builder.Services.AddHealthChecks()
.AddSqlServer(configuration.GetConnectionString("Default"))
.AddUrlGroup(new Uri("https://localhost:40443/health"), "CoreData Service")
.AddCheck<CustomHealthCheck>("custom");
app.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
Logging
[ApiController]
public class AssetController : ControllerBase
{
private readonly ILogger<AssetController> _logger;
[HttpGet("{id}")]
public async Task<ActionResult<Asset>> Get(Guid id)
{
using (_logger.BeginScope(new { AssetId = id }))
{
_logger.LogInformation("Getting asset {AssetId}", id);
try
{
var asset = await _service.GetAsync(id);
return Ok(asset);
}
catch (NotFoundException ex)
{
_logger.LogWarning(ex, "Asset {AssetId} not found", id);
return NotFound();
}
}
}
}
๐งช API Testing
Integration Testing
public class AssetApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
private readonly HttpClient _client;
public AssetApiTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
_client = _factory.CreateClient();
}
[Fact]
public async Task GetAssets_ReturnsSuccessAndCorrectContentType()
{
// Act
var response = await _client.GetAsync("/api/v1/assets");
// Assert
response.EnsureSuccessStatusCode();
Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType);
}
[Fact]
public async Task CreateAsset_ReturnsCreatedWithLocation()
{
// Arrange
var request = new { Name = "Test Asset", Type = "Equipment" };
// Act
var response = await _client.PostAsJsonAsync("/api/v1/assets", request);
// Assert
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
Assert.NotNull(response.Headers.Location);
}
}
๐ Resources
๐ Quick Reference
Generate API Client
kiota generate --language csharp --openapi ./openapi.yaml --output ./ApiClient --namespace Acsis.Components.Asset.ApiClient
Update OpenAPI Spec
# Export from running service
curl https://localhost:40443/openapi/v1.json > openapi.yaml
# Validate spec
openapi-validator openapi.yaml
Test API Endpoint
# Using curl
curl -X GET https://localhost:40443/api/v1/assets \
-H "Authorization: Bearer $TOKEN"
# Using httpie
http GET localhost:40443/api/v1/assets Authorization:"Bearer $TOKEN"