Robust .NET API development template at IATec with PostgreSQL persistence, promoting standard practices, efficiency and security. Ideal for scalable, high-performance APIs backed by PostgreSQL.
Current Version: 2.1.0
- About the Project
- Technologies and Stack
- Project Structure
- Architecture Layers
- Prerequisites
- How to Run
- Configuration
- API Documentation (Scalar)
- Health Checks
- API Versioning
- CORS
- Authentication
- Feature: Assets
- Logging Dispatcher
- External Services (AntiCorruption)
- Persistence Layer (PostgreSQL)
- Domain Layer
- Message Queue Layer
- CrossCutting Layer
- Test Projects
- Docker
- CI/CD
- Renaming the API
- Template Extension Points
- Contributing
- Changelog
This repository is a base template / scaffolding project for creating new .NET APIs with PostgreSQL persistence, following IATec standards. It is a fork of IATec.Standard.Net.Api with a fully implemented PostgreSQL persistence layer using EF Core, including dual Read/Write DbContext, entity mappings, snake_case naming convention, and an initial EF Core migration.
- Clean Architecture / Vertical Slices with MediatR CQRS.
- API Versioning via
Asp.Versioning.Mvc(query stringapi-version). - Scalar interactive API documentation (replaces Swagger).
- Health Checks with version endpoint.
- CORS policy (
AllowAnyOrigin,AllowAnyMethod,AllowAnyHeader). - FluentValidation pipeline behavior (validators run before handlers).
- Exception pipeline behavior via
IATec.Shared.Behaviors. - Logging dispatcher sending structured logs to IATec Log Service.
- Typed HttpClient for external IATec Log Service integration.
- PostgreSQL persistence with
Npgsql.EntityFrameworkCore.PostgreSQL— dualReadDataContextandWriteDataContextregistered. - Snake case naming convention via
EFCore.NamingConventions. - Domain model —
Peopleaggregate withPerson(aggregate root),Document(entity), and 5 value objects. - EF Core mappings —
PersonMappingandDocumentMappingviaIEntityTypeConfiguration<T>. - Initial EF Core migration (
20260121134832_Init) included. - Integration with internal
IATec.Shared.*packages.
- All command/query handlers in
Application/Features/Assets/throwNotImplementedException. MessageQueueDependencyInjectionConfigis empty.docker/Dockerfileanddocker/Local.Dockerfileare empty (0 bytes).- Test projects have no test framework packages (xUnit, NUnit, MSTest) and zero tests.
- No CI/CD pipelines (no
.github/workflows).
Note: Whenever creating a new API from this template, read the Renaming the API section to adjust names and references.
| Technology | Version |
|---|---|
| .NET | 10.0 |
| ASP.NET Core | 10.0.8 |
| Scalar.AspNetCore | 2.14.14 |
| Microsoft.AspNetCore.OpenApi | 10.0.8 |
| API Versioning (Asp.Versioning.Mvc) | 10.0.0 |
| Asp.Versioning.Mvc.ApiExplorer | 10.0.0 |
| AspNetCore.HealthChecks.UI.Client | 9.0.0 |
| MediatR | 14.1.0 |
| FluentValidation | 12.1.1 |
| FluentValidation.DependencyInjectionExtensions | 12.1.1 |
| FluentResults | 4.0.0 |
| Microsoft.EntityFrameworkCore | 10.0.2 |
| Microsoft.EntityFrameworkCore.Relational | 10.0.2 |
| Microsoft.EntityFrameworkCore.Tools | 10.0.2 |
| Npgsql.EntityFrameworkCore.PostgreSQL | 10.0.0 |
| EFCore.NamingConventions | 10.0.0 |
| Microsoft.Extensions.Configuration | 10.0.8 |
| Microsoft.Extensions.Configuration.Binder | 10.0.8 |
| Microsoft.Extensions.Http | 10.0.8 |
| Microsoft.Extensions.Options | 10.0.8 |
| IATec.Shared.Api | 1.2.0 |
| IATec.Shared.Application | 2.0.0 |
| IATec.Shared.Domain | 2.0.1 |
| IATec.Shared.HttpClient | 3.0.0 |
| IATec.Shared.Behaviors | 1.3.0 |
CSPROJ Settings:
Nullable=enable,ImplicitUsings=enable,InvariantGlobalization=false,GenerateDocumentationFile=True.
IATec.Standard.Net.Api.PostgreSQL/
├── src/
│ ├── Api/
│ │ ├── Configurations/
│ │ │ ├── ApiDependencyInjectionConfig.cs
│ │ │ └── Extensions/
│ │ │ ├── CorsPolicyExtension.cs
│ │ │ ├── HealthCheckExtension.cs
│ │ │ ├── MigrationExtensions.cs
│ │ │ ├── OptionsExtension.cs
│ │ │ ├── ScalarExtension.cs
│ │ │ └── VersioningExtension.cs
│ │ ├── Controllers/
│ │ │ └── (empty folder)
│ │ ├── Properties/
│ │ │ └── launchSettings.json
│ │ ├── appsettings.json
│ │ └── Program.cs
│ │
│ ├── Application/
│ │ ├── Configurations/
│ │ │ ├── ApplicationDependencyInjectionConfig.cs
│ │ │ ├── Extensions/
│ │ │ │ ├── MediatorConfig.cs
│ │ │ │ └── ValidatorConfig.cs
│ │ │ └── Factories/
│ │ │ └── ValidatorFactory.cs
│ │ ├── Dispatchers/
│ │ │ └── Logging/
│ │ │ └── LogDispatcher.cs
│ │ └── Features/
│ │ └── Assets/
│ │ ├── Commands/
│ │ │ ├── CreateAssetCommand.cs
│ │ │ └── CreateAssetCommandHandler.cs
│ │ ├── Queries/
│ │ │ ├── CheckIfExistsAssetQuery.cs
│ │ │ └── CheckIfExistsAssetQueryHandler.cs
│ │ └── Validators/
│ │ └── CreateAssetValidator.cs
│ │
│ ├── CrossCutting/
│ │ └── (empty — no .cs files)
│ │
│ ├── Domain/
│ │ └── Models/
│ │ └── People/
│ │ └── PeopleAggregate/
│ │ ├── Entities/
│ │ │ ├── Person.cs
│ │ │ └── Document.cs
│ │ └── ValueObjects/
│ │ ├── Documents/
│ │ │ ├── IssuerValue.cs
│ │ │ └── ValueValue.cs
│ │ └── Person/
│ │ ├── FirstNameValue.cs
│ │ ├── LastNameValue.cs
│ │ └── MiddleNameValue.cs
│ │
│ ├── Persistence/
│ │ ├── Configurations/
│ │ │ ├── PersistenceDependencyInjectionConfig.cs
│ │ │ ├── Extensions/
│ │ │ │ └── DatabaseExtension.cs
│ │ │ └── Options/
│ │ │ ├── EntityFrameworkOption.cs
│ │ │ └── PostgreSqlOption.cs
│ │ ├── Context/
│ │ │ ├── ReadDataContext.cs
│ │ │ └── WriteDataContext.cs
│ │ ├── Mappings/
│ │ │ └── People/
│ │ │ ├── PersonMapping.cs
│ │ │ └── DocumentMapping.cs
│ │ └── MIgrations/
│ │ ├── 20260121134832_Init.cs
│ │ ├── 20260121134832_Init.Designer.cs
│ │ └── WriteDataContextModelSnapshot.cs
│ │
│ ├── AntiCorruption/
│ │ ├── Configurations/
│ │ │ ├── AntiCorruptionDependencyInjectionConfig.cs
│ │ │ └── Extensions/
│ │ │ └── LoggingConfig.cs
│ │ └── Services/
│ │ └── Iatec/
│ │ └── LogService.cs
│ │
│ ├── MessageQueue/
│ │ └── Configurations/
│ │ └── MessageQueueDependencyInjectionConfig.cs
│ │
│ ├── Domain.Tests/
│ │ └── Domain.Tests.csproj (empty — no tests, no framework)
│ │
│ └── Application.Tests/
│ └── Application.Tests.csproj (empty — no tests, no framework)
│
├── docker/
│ ├── Dockerfile (empty — 0 bytes)
│ └── Local.Dockerfile (empty — 0 bytes)
│
├── secrets/
│ └── secrets.yml (Kubernetes Secret template)
│
├── IATec.Standard.Net.Api.sln
├── README.md
└── CHANGELOG.md
Entrypoint ASP.NET Core Web API. Orchestrates all configurations and maps controllers.
Contains use cases, MediatR handlers, validators, dispatchers, and factories.
Mediator Pipeline Behaviors (from IATec.Shared.Behaviors):
ValidatorPipelineBehavior<,>— runs FluentValidation validators before the handler.ExceptionPipelineBehavior<,>— global exception handling wrapper.
Contains the People aggregate — the only implemented domain model in this template.
| Type | Class | Description |
|---|---|---|
| Aggregate root | Person |
Extends EntityUlidInt32. Has FirstName, MiddleName?, LastName, Age, and a collection of Document. |
| Entity | Document |
Extends EntityUlidInt32. Has PersonId, Value, Issuer, and factory method Document.Create(...). |
| Value object | FirstNameValue |
string Value, min 1 / max 50 chars. |
| Value object | LastNameValue |
string Value, min 1 / max 50 chars. |
| Value object | MiddleNameValue |
string Value, nullable in Person. |
| Value object | IssuerValue |
string Value, document issuer. |
| Value object | ValueValue |
string Value, document value/number. |
Fully implemented PostgreSQL persistence layer using EF Core + Npgsql. Registered in PersistenceDependencyInjectionConfig.cs via DatabaseExtension.cs:
| Context | Registration | Connection | Purpose |
|---|---|---|---|
ReadDataContext |
DbContext |
ServerReader |
Read-only queries with NoTrackingWithIdentityResolution |
WriteDataContext |
DbContext |
ServerWriter |
Write operations with change tracking |
Both contexts apply snake_case naming via .UseSnakeCaseNamingConvention() and load entity mappings via ApplyConfigurationsFromAssembly(...).
Adapters for external services. Currently implements:
- IATec Log Service (
LogService.cs) — typedHttpClientsendingLogDtoviaPOST v1/log.
Currently empty scaffolding. ConfigureMessageQueue() returns services with no registrations.
Currently empty. Contains only CrossCutting.csproj with references to IATec.Shared.Behaviors (1.3.0) and MediatR (14.1.0). Zero .cs source files.
- .NET 10 SDK
- PostgreSQL instance running locally or remotely
- Editor of your choice (VS, VS Code, Rider)
git clone <repository-url>
cd {API_NAME}Ensure a PostgreSQL instance is running. Update appsettings.json with the correct ServerReader, ServerWriter, User, Password, and Database.
dotnet ef database update --project src/Persistence --startup-project src/Api --context WriteDataContextdotnet restoredotnet run --project src/Api/Api.csprojDefault profile (from launchSettings.json):
- URL:
http://localhost:5015 - Environment:
Local - Auto-opens browser at
/documentation
Settings are located in src/Api/appsettings.json.
| Section | Description | Example |
|---|---|---|
TimeZone |
Application time zone | "America/Sao_Paulo" |
Container.Name |
Deployment container metadata name | "MyApi-Production" |
Container.ContainerId |
Container identifier | "my-api-container" |
Logging.LogLevel.Default |
ASP.NET Core log level | "Debug", "Information", "Warning" |
PostgreSQL.Database |
PostgreSQL database name | "myapp_prod" |
PostgreSQL.User |
PostgreSQL user | "myuser" |
PostgreSQL.Password |
PostgreSQL password | "..." |
PostgreSQL.ServerReader |
Read replica host | "pg-read.internal" |
PostgreSQL.ServerWriter |
Primary/write host | "pg-write.internal" |
PostgreSQL.Port |
PostgreSQL port | "5432" |
EntityFramework.SensitiveDataLogging |
Enable EF Core sensitive logging | false in production |
In OptionsExtension.cs:
| Option Class | Configuration Key | Purpose |
|---|---|---|
LogServiceOption |
LogServiceOption |
URL for IATec Log Service |
ContainerOption |
ContainerOption |
Container metadata for logging dispatcher |
In PersistenceDependencyInjectionConfig.cs:
| Option Class | Configuration Key | Purpose |
|---|---|---|
EntityFrameworkOption |
EntityFramework |
EF Core settings (SensitiveDataLogging) |
PostgreSqlOption |
PostgreSQL |
Host, port, database, credentials. Builds connection string via GetConnectionString(isReader) |
Tip: Add new typed options in
src/Api/Configurations/Extensions/OptionsExtension.csfor injection viaIOptions<T>.
The project uses Scalar (not Swagger) for interactive API documentation.
- OpenAPI JSON:
/openapi/v1.json - Scalar UI:
/documentation - Availability: Only in non-Production environments (
!app.Environment.IsProduction()). - Theme: Mars, dark mode, C# HttpClient as default client.
Endpoint: GET /_healthcheck/status
- VersionHealthCheck — returns
HealthStatus.Healthywith the assembly version string. - Response is formatted via
HealthChecks.UI.Client(rich JSON with UI metadata).
Configured in VersioningExtension.cs:
- Default version:
1.0 - Assume default when unspecified:
true - Report API versions:
true - Version reader: Query string parameter
api-version - Explorer group format:
'v'VVV(e.g.,v1)
Configured in CorsPolicyExtension.cs with AllowAnyHeader, AllowAnyMethod, AllowAnyOrigin.
Exposed headers: X-Custom-Header, Location, Content-Disposition, Content-Length.
⚠️ Warning:AllowAnyOrigin()is extremely permissive. Review and restrict before deploying to production.
Currently NOT configured.
JWT Bearer setup is not present. To add JWT authentication:
- Install
Microsoft.AspNetCore.Authentication.JwtBearer. - Configure token validation in
ApiDependencyInjectionConfig.csor a new extension method. - Add
app.UseAuthentication()beforeapp.UseAuthorization()inUseApi(). - Add security schemes to the OpenAPI document in
ScalarExtension.cs.
The only implemented feature domain in the Application layer. All handlers are placeholders (NotImplementedException).
| File | Type | Details |
|---|---|---|
CreateAssetCommand.cs |
readonly record struct |
IRequest<Result> from MediatR + FluentResults |
CreateAssetCommandHandler.cs |
Handler | throw new NotImplementedException() |
CheckIfExistsAssetQuery.cs |
readonly record struct |
IRequest<Result<bool>> |
CheckIfExistsAssetQueryHandler.cs |
Handler | throw new NotImplementedException() |
CreateAssetValidator.cs |
AbstractValidator<CreateAssetCommand> |
Empty — no rules defined |
Replace this feature with your own domain features when cloning this template.
Implements ILogDispatcher (from IATec.Shared.Domain.Contracts.Dispatcher).
Constructs a LogDto with:
ContainerKey— fromContainerOption.KeySource,Owner,Action— caller-providedContent—object?.ToString() ?? string.EmptyDate—DateTime.UtcNowId—string.EmptyUserId—string.Empty
Then delegates to ILogService.SendAsync().
LogService.cs (AntiCorruption layer):
- Typed
HttpClientwith base address fromLogServiceOption.Url. - Sends
POST v1/logas JSON. - Does NOT throw on failure — errors are caught and logged silently.
| Config | File |
|---|---|
| DI Registration | LoggingConfig.cs |
| Implementation | LogService.cs |
| Contract | ILogService (from IATec.Shared.Domain) |
DatabaseExtension.cs registers two EF Core contexts backed by Npgsql:
ReadDataContext— configured withNoTrackingWithIdentityResolutionquery tracking behavior. Use for all read queries to avoid unnecessary change tracking overhead.WriteDataContext— configured with default change tracking. Use for all write operations (insert, update, delete).
Both contexts use .UseSnakeCaseNamingConvention() (all column/table names in snake_case) and .UseNpgsql(connectionString).
Example: injecting contexts in a repository:
public class PersonRepository(ReadDataContext read, WriteDataContext write)
{
public async Task<Person?> GetByIdAsync(int id)
=> await read.Set<Person>().FirstOrDefaultAsync(p => p.Id == id);
public async Task SaveAsync(Person person)
{
await write.Set<Person>().AddAsync(person);
await write.SaveChangesAsync();
}
}One migration is included: 20260121134832_Init — creates the initial people.person table.
To add a new migration:
dotnet ef migrations add MigrationName --project src/Persistence --startup-project src/Api --context WriteDataContext
dotnet ef database update --project src/Persistence --startup-project src/Api --context WriteDataContextEF Core mappings are implemented via IEntityTypeConfiguration<T> in src/Persistence/Mappings/:
| Mapping | Entity | Table |
|---|---|---|
PersonMapping |
Person |
people.person |
DocumentMapping |
Document |
Applied via assembly scan |
PersonMapping maps value objects (FirstName, MiddleName, LastName) using .OwnsOne(...) and configures column types (varchar, char) and constraints.
Contains the People aggregate — the domain model for this template.
Extends EntityUlidInt32. Holds FirstName, MiddleName?, LastName, Age, and a private List<Document> exposed as IReadOnlyCollection<Document> Documents.
Extends EntityUlidInt32. Has PersonId, Value, Issuer. Created via factory method Document.Create(personId, value, issuer).
| Class | Property | Constraints |
|---|---|---|
FirstNameValue |
string Value |
FieldMinLength = 1, FieldMaxLength = 50 |
LastNameValue |
string Value |
FieldMinLength = 1, FieldMaxLength = 50 |
MiddleNameValue |
string Value |
FieldMinLength = 1, FieldMaxLength = 50 |
IssuerValue |
string Value |
Document issuer identifier |
ValueValue |
string Value |
Document value/number |
Status: Completely empty. ConfigureMessageQueue() returns services with no registrations. No message queue library referenced.
Status: Completely empty.
Contains only CrossCutting.csproj with references to IATec.Shared.Behaviors (1.3.0) and MediatR (14.1.0). Zero .cs source files.
| Project | Framework Packages | Test Files | Status |
|---|---|---|---|
Domain.Tests |
None | 0 | Empty |
Application.Tests |
None | 0 | Empty |
dotnet add src/Domain.Tests package xunit
dotnet add src/Domain.Tests package Microsoft.NET.Test.Sdk
dotnet add src/Domain.Tests package xunit.runner.visualstudioEmpty (0 bytes).
Empty (0 bytes).
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
EXPOSE 8080
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore "src/Api/Api.csproj"
RUN dotnet build "src/Api/Api.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "src/Api/Api.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Api.dll"]Status: No CI/CD configured.
- No
.github/folder. - No GitHub Actions workflows.
- No Azure DevOps pipelines.
- No Jenkins/GitLab CI files.
The only file in secrets/ is secrets.yml, a Kubernetes Secret manifest template with placeholders (#{deployment_name}#, #{namespace}#).
Follow these steps when using this project as a base for a new API.
mv IATec.Standard.Net.Api.sln {API_NAME}.slnUpdate Api.csproj if desired:
<AssemblyName>{API_NAME}.Api</AssemblyName>
<RootNamespace>{API_NAME}.Api</RootNamespace>Run a Replace All in src/ to adjust namespaces:
| From | To (example) |
|---|---|
namespace Api; |
namespace MyProject.Api; |
namespace Application; |
namespace MyProject.Application; |
namespace Persistence; |
namespace MyProject.Persistence; |
namespace Domain; |
namespace MyProject.Domain; |
Or keep simplified namespaces (Api, Application, Domain, etc.).
In src/Api/Configurations/Extensions/ScalarExtension.cs, replace {API_NAME} with the actual project name.
In appsettings.json, update PostgreSQL.Database with your own database name. Update table/schema names in entity mappings (PersonMapping.cs, etc.) accordingly.
Replace all {API_NAME} placeholders with the actual project name.
Update <Version> in Api.csproj to 1.0.0 for the new project.
This project is an intentional scaffold/template. The following items are structural placeholders — not bugs or missing features. Each represents an extension point where a new API project should add its own implementation.
| # | Extension Point | Location | Purpose |
|---|---|---|---|
| 1 | NotImplementedException handlers |
Application/Features/Assets/ |
Example command/query structure — replace with real business logic |
| 2 | Empty MessageQueue layer |
src/MessageQueue/ |
Add producers/consumers (RabbitMQ, Kafka, etc.) |
| 3 | Empty CrossCutting layer |
src/CrossCutting/ |
Add shared utilities, constants, or cross-cutting concerns |
| 4 | Empty test projects | *.Tests/ |
Add xUnit/NUnit/MSTest and write tests |
| 5 | No CI/CD | Root | Add GitHub Actions / Azure DevOps when ready |
| 6 | Empty Dockerfiles | docker/ |
Add build/publish steps for containerization |
| 7 | Permissive CORS | CorsPolicyExtension.cs |
Restrict origins/methods when deploying to production |
| 8 | No authentication | ApiDependencyInjectionConfig.cs |
Add JWT/Auth when security requirements are defined |
| 9 | {API_NAME} placeholders |
ScalarExtension.cs, README |
Rename when cloning template |
| 10 | No appsettings.Development.json |
src/Api/ |
Create environment-specific configs as needed |
| 11 | People domain model |
src/Domain/Models/People/ |
Replace or extend with the actual domain aggregate of the new API |
| 12 | SensitiveDataLogging: true |
appsettings.json |
Set to false before deploying to production |
Contributions are welcome! To contribute:
- Fork the repository.
- Create a branch:
git checkout -b feature/feature-name. - Commit with clear messages following Conventional Commits.
- Open a Pull Request for review.
See CHANGELOG.md for full release history.