Altinn 3 is being built as a distributed application consisting of several smaller services. While this has some upsides, there are also a few complications that arrises from having such an architecture, one of which is how to do testing during development.
Testing in this context refers to the typical testing done by a developer to check that a feature works as expected while developing said feature. This is unrelated to acceptance testing, quality assurance and automated testing.
For instance, when working on the access-management, one typically needs to have the following running:
- authorization
- resource registry
- profile
- register
all of which requires data to be in sync, weather through mocking, through calling Altinn 2, or though having test-data available somehow. This can lead to longer onboarding, and frustration for developers.
At other times, one needs to make changes across multiple projects. For instance, access-management might require register to add a new API that access-management then uses. For these cases, and for bugs that cross service boundaries, it's very valuable to be able to debug across service boundaries.
We would like to be able to work on individual projects (like access-management), locally, without having to know about all of the dependencies of access-management, and without having to define a lot of test-data oneself. At the same time, we would also like to be able to develop on multiple projects at once when the need arises.
The following are features desired from a system designed to improve the current situation:
- After cloning a product repository, one should be able to run it as is, without having to install/setup other dependencies.
- Running the product in dev mode locally should have test data available out of the box.
- Changing only a little configuration at most, one should be able to run against a modified local dependency (also in dev mode, with available test data).
We would like to propose a solution that consists of a few different parts, and that we think will solve all of the success criterias listed above. The proposed solution can be rolled out in phases, to products as the need arrises, and is not an all-or-nothing solution. That being said; all the parts are required, at least across a few different repositories, if all of the success criterias are to be met.
.NET Aspire is a currently in-preview (expected release first half of 2024) solution for building and deploying distributed application. It contains out of the box configuration and support for industry standard technologies like OpenTelemetry for tracing and metrics. It also supports features such as service-discovery that enables a standardized way for applications to be informed about where their dependencies are located.
Aspire can be used for both running and deploying distributed applications, but at this point in time we're not interested in the deploying part. Instead, we're interested in what it enables for local development. When setting up an application with .NET Aspire, one creates an "app host" project that describes all the different parts required to run the application, and how all of these parts ties together. The example below for instance, showcases how resource-registry is configured to run with a postgres database that Aspire manages:
var builder = DistributedApplication.CreateBuilder(args);
var dbServer = builder.AddPostgresContainer("db", port: 11059, password: "resource_registry_local_db_password")
.WithPersistentData("resourceregistry-db");
var resourceRegistryDb = dbServer.CreateDatabase("resourceregistry_db", "resourceregistry");
builder.AddProject<Projects.Altinn_ResourceRegistry>("api")
.WithReference(resourceRegistryDb);
builder.Build().Run();
When this application is run, both a database server (in docker), and the resource registry is started together. The resource registry is started in development mode with the debugger attached, and the connection string for the database is provided to the application by Aspire. Aspire also provides other configuration values such as how the application should log, and where it should send metrics used by Aspire to generate the following dashboard:
This solves the first criteria of bing able to run a product right out of the box, becuase the database and other dependencies are handled by aspire. It also potentially solves the third criteria, though that will be discussed in more detail futher down.
To solve the second success criteria, we have created a proposed test-data seeder that can be used to easily seed test data to local databases when running locally. It's designed to be versatile and configurable, though it only supports postgresql. The test-data seeder is designed to make it very easy to run SQL scripts if-and-only-if a given table is empty. It also allows declaring prepare and finalize scripts that can be used to disable indexes or similar while data is beeing seeded, to work around temporarily breaking constraints.
This solution will make it the responsibility of a product's owner to ensure that it contains a set of test-data that can be used during local development. For this to be at all a practical solution, automation will have to be put in place to ensure that the test-data does not go out of sync with the database schema. Our suggested solution is to have tests in CI which spins up a temporary database, runs migration scripts and then runs the data seeder to check that the test data is never broken.
Test data will also have to be kept somewhat in sync between teams and/or repositories. For instance, access-management might have test data that references an organization. That organization should then exist in register's test data.
The suggested way of generating the test data is to run the product locally against a local database, and then use APIs to insert data into the local database. Database tools like DBeaver can then be used to generate SQL scripts from existing database tables. For certain products it might also be a good idea to export datasets from Altinn 2. All the data that are to be used as test data needs to be free of personal information, and safe to put up on the open internet (as it will recide in sql files on github as part of the product).
The different Altinn 3 applications/products should support a "local development" mode (simply refered to as LocalDev
from here on). This mode should only be allowed when the application is also running in development mode (ASPNET_ENVIRONMENT=Development
).
When an application is running in LocalDev
mode, a few properties should be met:
-
The application should not need to call any Altinn 2 APIs that cannot be called from any IP address. Prefferably, the application should not need to call any Altinn 2 APIs at all.
-
Applications that typically get data from Altinn 2 should instead provide run against a local "local-dev" database, which can be seeded with test-data (see section above). This can be incrementally adopted and expanded on when needs arise from dependents.
-
API authorization needs to enable some form of local-generatable token. This can probably be achieved by injecting JWK into the application through configuration, but the details of this has not been looked into yet and needs to be expanded on. A local, offline, token-generator might also be warranted.
-
Applications can provide additional APIs (under a
/localdev/
prefix) to allow inserting and modifying test data. This is optional and would typically be implemented on demand by dependents. Great care needs to be taken to ensure these APIs are not activated when the application is not running inLocalDev
mode, and as such it is strongly recommend that a library be created that deals with enabling/disabling these APIs, and ensure they are also not part of the normal swagger document.
Note that the reason and role of LocalDev mode is not to run the application that's under development in this mode, but rather all of it's dependencies. So for instance, if one is working on resource-registry
, one could run register
in LocalDev mode.
The third part to our proposed solution is to improve and (somewhat) standardize the application configuration of the different applications. This involves a few different suggested changes to make the previous three points work better.
First off, we suggest using the standard .NET way of providing connection strings to the application. Today, the connection strings for resource-registry is provided as two separate components: PostgreSQLSettings:ConnectionString
, and PostgreSQLSettings:AuthorizationDbPwd
. This has a few different issues (in addition to the fact that it's non-standard). First of, Aspire assumes that applications will use standard connection-string names. While this can definitely be worked around, doing so requires more code in each application which is unnesesary. The other (bigger) problem is that all of the applications requires the same name. This means that if shared configuration is injected into all of the applications, all of them will use the same connection string, instead of separate ones. For this reason, we suggest standardizing on ConnectionStrings:<product_name>_db
for databases, ConnectionStrings:<product_name>_db_migrator
for the database migrator, and similar for other connection strings, but with a different suffix. For local testing, a ConnectionStrings:local_db_server
will also be introduced to enable applications to create their own database on the database server.
Second, a Altinn
key should be supported in all products, with a common set of configuration values. This includes (but is not limited to):
-
EnableLocalDevMode
- whether or not to enable local dev mode. This is only supported if environment is development. SettingEnableLocalDevMode
to true without setting environment to development should cause an exception and the application will fail to start. -
SeedTestData
- defaults to true ifEnableLocalDevMode
is true, only supported if environment is development. SettingSeedTestData
to true without setting environment to development should cause an exception and the application will fail to start.
More config normalizations will probably appear as this work continues across more repositories, and they should be documented here.
Obviously, we can't just flip a switch and have all of the features described in this document enabled for all of Altinn 3 services. However, the further along Altinn 3 gets, and the more products gets developed, the worse the situation is going to get. Therefore, we recommend starting the foundational work immediately. That being said, not everything needs to be done at once, and different parts of this solution could be rolled out in stages. Similarly, the problems could be tackled on different products one by one instead of all at once. Our proposed path forward is as following:
The first step should probably be to create a new github repository where NuGet packages can be published. The first couple of packages to be created here are:
- Test-Data Seeder (name tbd.)
- Altinn.Configuration.ServiceDefaults
The first one should contain the data seeder as described in similarly named section above, and the second one should contain shared configuration and extension methods, such that setting up a Altinn 3 API could contain a single builder.AddAltinnServiceDefaults("name of service")
to configure most of the shared settings for Altinn services. This should include configuring logging, and instrumentation both for use with Aspire when developing locally, but also for use with AppInsights when running in environments.
This should also configure databases for projects that need them, including handling connection strings, creating the database if needed in local development mode, migrating the data using whatever data migrator the project is using, and seeding data if enabled.
Publishing of these packages to a Nuget package registry needs to be automated. These can either be pushed to the central nuget package repository, or another open package registry like github's. Our recommendation is to publish pre-release version (main
builds) to github's package registry, and publish proper releases (tags) to nuget.org
.
After these two libraries has been created, they should be added to a product to properly prove the concept. Our testing and exploration to date has used the resource registry project, and we think this is a good candidate for a first adopter as it has few dependencies and is fairly small. One requirement for this to be considered done is that CI needs to fail if the Test-Data is ever out of sync with the database schema.
After the basic setup for local development (without dependencies) has been done for a product, it's dependencies needs to be updated to support LocalDev
mode. This most likely also means consuming the Altinn.Configuration.ServiceDefaults
package. These dependencies also needs to be setup to publish images to a image registry that is open to consuming from unauthorized clients. This will allow running for instance resource registry to pull an image of register if it is not available locally. Publishing of these images needs to be automated. We suggest publishing to ghcr.io
from github actions.
After images are available for the dependencies, the product from step 2 should be updated to run it's dependencies automatically using Aspire. This should allow running them either as docker images (the default), or as local projects. This should be configuration based, where it's possible to provide a relative or absolute path to find local projects. Example Aspire configuration:
{
// Path relative to current project's repository root
"Altinn:register:ProjectPath": "../altinn-register/src/Altinn.Register"
}