Skip to content

Latest commit

 

History

History
239 lines (167 loc) · 10.1 KB

File metadata and controls

239 lines (167 loc) · 10.1 KB

📖 Introduction

Receiver is a repository to receive meldinger / notifications / messages from different sources. The term 'meldinger' is by intention not translated to be consistent with naming conventions within the domain. The main source to receive meldinger from is Digdirs Altinn plattform, which is why this project is using several altinn related terms, also within the domain model.

🚀 Start the application

The application exposes a scalar ui, which is locally accesible on http://localhost:9008/scalar/v1. Discover the available endpoints on the UI or use the OpenApi Spec. The OpenApi Spec can be imported by other tools like e.g. Postman.

Local Development (for developers)

Prerequisites:

  • Docker installed
  • dotnet SDK installed (v10)
  • dotnet-ef tool (dotnet tool install --global dotnet-ef)

To test and debug locally, you need to run the following commands:

Run dotnet / dotnet ef commands from the src/ directory (the solution MeldingerReceiver.slnx lives there). Run docker compose and pnpm commands from the repository root.

docker compose -f compose.infra.yaml --profile monitoring up -d

The monitoring profile is optional and can be dropped. If used, a telemetry backend as defined in the opentelemetry section is spinned up.

This starts (as per today) a plain postgres instance without any seed, a common valkey service as well as en emulator for the managed google cloud storage service.

Before starting the actual application, we need to create quartz tables for persisting scheduled jobs (this also needs to be done whenever you have changed something withing the entity model, just remember to adjust the migration name):

dotnet ef migrations add InitDb --startup-project App/src --project Infrastructure/src -o Db/Migrations

Now, start the actual asp dotnet core application:

dotnet run --project App/src

Local Development in a complete dockerized environment (for e.g tester)

Prerequisites:

  • Docker installed
  • dotnet ef migrations have to be up to date / migration files need to be available (see previous section)

Start the application by running:

docker compose up --build -d

Cleanup

If you want to stop all containers, run:

docker compose down

If you want to get a clean database at the next startup, simply remove all the attached volumes:

docker compose down -v

🏃‍♂️ Getting Started

🛠️ Build and Test

In order to ensure that the intended architecture is maintained, there are some basic ArchUnit.Tests (docs) which will ensure that the dependencies do not cross the boundaries established by hexagonal architecture. Enhance the ArchUnit tests with other guidelines you want to maintain, like Namespaces, Naming Conventions and so on.

Also, a couple of sample tests in the different modules are provided to show how things can be tested. In order to run the repository tests, a running docker instance is required. (E.g. Docker Desktop)

dotnet test

🧱 Project Structure

.
└── src
    ├── App
    │   ├── src
    │   └── test
    ├── ArchUnit.Tests
    ├── Domain
    │   ├── Data
    │   ├── Logic
    │   │   ├── src
    │   │   └── test
    │   └── Ports
    │       ├── App
    │       └── Infrastructure
    ├── Infrastructure
    │   ├── src
    │   └── test
    ├── Publish
    │   └── Receiver.Publish
    ├── Tools
    │   └── Tools.GenerateOpenApi
    ├── generated
    └── npm_publish
  • ArchUnit.Tests
    • Important tests to gurantee the below structure keeps maintained
  • Domain
    • Domain.Logic implements Domain.Ports.App and uses Domain.Ports.Infrastructure
    • Domain.Logic.Test contains tests that validate the domain logic
    • Domain.Data contains Classes/DTOs which can be shared across layers
  • Infrastructure (Outgoing: infrastructure the application talks with)
    • Infrastructure implements Domain.Ports.Infrastructure (i.e. Adapters)
    • Infrastructure.Test contains tests that validate the Infrastructure
  • App (Incoming: adapters to make it possible to talk with the application)
    • App uses Domain.Ports.App
    • Responsible for injecting the necessary dependencies and exposing API endpoints
    • App.Test contains typically integration tests
  • Tools
    • Various tools that are used for development, e.g. OpenAPI generation

Domain.Logic and Infrastructure implementations are internal, and only exposed through DependencyInjection extensions.

👩‍💻 Logging

We want to use structured Logging in order to read, filter and query logs in an easy manner. See Logging in C# and .NET. That means, if you are not in the API Adapter Layer (or not where you start the application), you can simply inject a logger Instance via the constructor of a class (like ILogger<ExampleClass> logger). Feel free to write logging extension methods in order to streamline your logging even more.

In all places where you can`t use DependencyInjection, a logger instance can be created the following way:

ILogger<Function> logger = LoggerFactory.Create(builder => builder.AddConsole().AddJsonConsole()).CreateLogger<Function>();

To ensure that our logging stays consistent, there are ArchUnit tests which check if other logging mechanisms are used and which will fail in that case.

🔎 Observability / OpenTelemetry

General information about observability

We strongly recommend to use the default opentelemetry setup in this application. It ensures a minimal level of observability and can be fine-tuned if needed. When running docker-compose with the monitoring profile, a default backend of a opentelemetry-collector, mimir/prometheus (Metrics), loki (Logs), tempo (Traces) and grafana (Dashboard) is started. You will find the same or a similar setup in our production environment. To explore the telemetry data your application is producing, check out Grafana Explore.

Traces

Checkout OpenTelemetry .NET Traces for best practices, fine-tuning and examples.

Configuration example

services.AddOpenTelemetry()
        .WithTracing(options =>
            {
                options.AddAspNetCoreInstrumentation();
                options.AddHttpClientInstrumentation();
                options.AddEntityFrameworkCoreInstrumentation();
                options.AddNpgsql();
                options.AddOtlpExporter();
            })

Logs

Checkout OpenTelemetry .NET Logs for best practices, fine-tuning and examples.

Configuration example

services.AddOpenTelemetry()
        .WithLogging(
            logging => logging.AddOtlpExporter(),
            options => options.IncludeFormattedMessage = true
        );

Metrics

Checkout OpenTelemetry .NET Metrics for best practices, fine-tuning and examples.

Configuration example

services.AddOpenTelemetry()
        .WithMetrics(options =>
            {
                options.AddAspNetCoreInstrumentation();
                options.AddHttpClientInstrumentation();
                options.AddOtlpExporter();
            });

📦 TypeScript Type Generation

This template includes automated TypeScript type generation from your OpenAPI specification. The generated types can be used to create clients or for type-safe interactions with the API.

Setup and Customization

  1. Type generation tooling is configured in package.json.
  2. Customize the OpenAPI specification in App/src/Extensions/StartupExtensions.cs, where ConfigureApi() calls ConfigureBasicOpenApiSpec(...).
  3. Adjust TypeScript generation options in package.json. Update the generate:ts script to customize openapi-typescript behavior as needed.

Generate TypeScript Types

Generate types on-demand:

pnpm install # Run this once to install dependencies
pnpm generate:types

This will:

  1. Generate the OpenAPI specification in src/generated/openApi.json
  2. Generate TypeScript type definitions in src/generated/types.d.ts

Publish TypeScript Types

The publishable npm package root is npm_publish. It is published alongside receiver releases and uses the version from Publish/Receiver.Publish/Receiver.Publish.csproj, so the committed npm package version is only a placeholder and should not be bumped manually.

Prerelease receiver versions (*-*) are published with the next npm tag. Stable receiver versions use npm's default tag.

⚠️ CI/CD Considerations

If you have dependent steps, such as package publishing, ensure that the generated types are up to date:

pnpm generate:types && git diff --exit-code

📝 Further reads

Medium: Hexagonal Architecture, there are always two sides to every story