Table of Contents

Build your first Arc4u app

In this tutorial you build a minimal ASP.NET Core API with two endpoints: GET /hello/{name}, open to anyone, and GET /me, which requires a JWT bearer token. Along the way you use four Arc4u features:

  • Dependency injection: a service registered with the [Export] attribute and a source generator.
  • Configuration: application settings bound and validated at startup.
  • Logging: the Arc4u logger, which adds a category and context to each entry, on top of Serilog.
  • Authentication: JWT bearer validation that rejects unauthenticated calls with a JSON error body.

It takes about 15 minutes. You do not need an identity provider: without a token, /me answers 401, which is what you check at the end. Call the protected endpoint with a real token shows how to connect one.

Prerequisites

  • The .NET 10 SDK or later (dotnet --version).
  • curl, or any other HTTP client.
  • No prior Arc4u knowledge. Install Arc4u describes the packages in more detail; this tutorial repeats the commands you need.

Step 1: Create the project

Create an empty ASP.NET Core project and move into its folder:

dotnet new web -n GettingStarted
cd GettingStarted

Keep the name GettingStarted: the source generator names the registration method after the assembly (see Step 5).

Step 2: Add the packages

dotnet add package Arc4u.Dependency --prerelease
dotnet add package Arc4u.Dependency.Tool --prerelease
dotnet add package Arc4u.Configuration --prerelease
dotnet add package Arc4u.Diagnostics --prerelease
dotnet add package Arc4u.OAuth2.AspNetCore.Authentication --prerelease
dotnet add package Serilog.AspNetCore
Package Used for
Arc4u.Dependency The [Export], [Scoped] and [Shared] attributes.
Arc4u.Dependency.Tool The source generator that turns those attributes into registrations.
Arc4u.Configuration AddApplicationConfig, which binds the application settings.
Arc4u.Diagnostics The Arc4u logger (AddILogger, Technical(), Business()).
Arc4u.OAuth2.AspNetCore.Authentication AddJwtAuthentication, the JWT bearer setup.
Serilog.AspNetCore Serilog and its console sink, which write the log entries.

dotnet list package now shows the six packages.

Step 3: Configure the application

Replace the content of appsettings.json with:

{
  "Urls": "http://localhost:5080",
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft.AspNetCore": "Warning"
      }
    }
  },
  "Application.Configuration": {
    "ApplicationName": "GettingStarted",
    "Environment": {
      "Name": "Local",
      "LoggingName": "GettingStarted",
      "TimeZone": "UTC"
    }
  },
  "Authentication": {
    "DefaultAuthority": {
      "Url": "https://login.example.com/realms/getting-started"
    },
    "OAuth2.Settings": {
      "Audiences": [ "getting-started-api" ]
    }
  }
}
Key Read by Description
Urls ASP.NET Core The address the application listens on. It takes precedence over the random port in Properties/launchSettings.json.
Serilog:MinimumLevel Serilog Minimum log levels: Information, and Warning for ASP.NET Core's own entries.
Application.Configuration AddApplicationConfig The application name and environment. The section and its four values are required: when a value is missing, startup fails with a ConfigurationException that lists the missing ones. Environment:LoggingName is written in every log entry as Application.
Authentication:DefaultAuthority:Url AddJwtAuthentication The identity provider that issues the tokens. The placeholder is enough to start: the application contacts the identity provider only to validate a token.
Authentication:OAuth2.Settings:Audiences AddJwtAuthentication The values accepted in the aud claim of a token.

Application.Configuration and OAuth2.Settings are single keys that contain a dot, not nested sections.

Step 4: Add a service

Create Services/GreetingService.cs:

using Arc4u.Configuration;
using Arc4u.Dependency.Attribute;
using Arc4u.Diagnostics;
using Microsoft.Extensions.Options;

namespace GettingStarted.Services;

public interface IGreetingService
{
    string Greet(string name);
}

[Export(typeof(IGreetingService)), Scoped]
public sealed class GreetingService(ILogger<GreetingService> logger, IOptions<ApplicationConfig> config) : IGreetingService
{
    public string Greet(string name)
    {
        logger.Business().LogInformation("Greeting {Name}", name);

        return $"Hello {name}, from {config.Value.ApplicationName} ({config.Value.Environment.Name}).";
    }
}
  • [Export(typeof(IGreetingService))] registers GreetingService as the implementation of IGreetingService. [Scoped] gives it a scoped lifetime; use [Shared] for a singleton, or no lifetime attribute for a transient.
  • IOptions<ApplicationConfig> gives the values of the Application.Configuration section.
  • logger.Business() marks the entry with the Business category. Technical() and Monitoring() are the other categories.

Step 5: Write Program.cs

Replace the content of Program.cs with:

using System.Security.Claims;
using Arc4u.Configuration;
using Arc4u.Dependency;
using Arc4u.OAuth2.Extensions;
using GettingStarted.Services;
using Serilog;

var builder = WebApplication.CreateBuilder(args);

// Serilog writes the log events; the Arc4u logger adds Category, SourceContext, Method and Application to each one.
builder.Services.AddSerilog(logger => logger
    .ReadFrom.Configuration(builder.Configuration)
    .WriteTo.Console(outputTemplate:
        "[{Timestamp:HH:mm:ss} {Level:u3}] {Category,-10} {SourceContext}: {Message:lj}{NewLine}{Exception}"));
builder.Services.AddILogger();

// Binds the Application.Configuration section to IOptions<ApplicationConfig>.
builder.Services.AddApplicationConfig(builder.Configuration);

// Generated at compile time by Arc4u.Dependency.Tool from the [Export] attributes of this assembly.
builder.Services.RegisterGettingStartedTypes();

// Validates JWT bearer tokens issued by the authority in the Authentication section.
builder.Services.AddJwtAuthentication(builder.Configuration);
builder.Services.AddAuthorization();

// Errors without a body (404, unhandled exceptions, ...) become ProblemDetails responses.
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler();
app.UseStatusCodePages();
app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/hello/{name}", (string name, IGreetingService greetings) => greetings.Greet(name));

app.MapGet("/me", (ClaimsPrincipal user) => user.Claims.Select(claim => new { claim.Type, claim.Value }))
   .RequireAuthorization();

app.Run();

What each part does:

Logging. AddSerilog makes Serilog write every log entry, here to the console. AddILogger replaces the ILogger<T> that ASP.NET Core injects with the Arc4u logger. The Arc4u logger adds Category, SourceContext (the class), Method and Application to each entry, and provides Technical(), Business() and Monitoring(). Those methods throw InvalidOperationException: Bad Arc4u usage. when AddILogger is missing, and AddJwtAuthentication logs through them, so call AddILogger in every Arc4u application.

Configuration. AddApplicationConfig reads the Application.Configuration section, checks that the four values are set, and registers IOptions<ApplicationConfig>. Pass sectionName to read another section.

Dependency injection. Arc4u.Dependency.Tool scans the classes of your project for [Export] when you compile and generates one extension method, Register<Name>Types, in the Arc4u.Dependency namespace. <Name> is the last dot-separated part of the assembly name: GettingStarted gives RegisterGettingStartedTypes, and Contoso.Orders.Api would give RegisterApiTypes. For this project, the generated method contains:

services.AddScoped<GettingStarted.Services.IGreetingService, global::GettingStarted.Services.GreetingService>();

Authentication. AddJwtAuthentication reads the Authentication section and registers the ASP.NET Core JWT bearer handler as the default scheme, with Arc4u's StandardBearerEvents. When a request to a protected endpoint has no valid token, StandardBearerEvents answers 401 with a JSON body, and it logs why a token was rejected. It validates the token issuer too; see the caution in Call the protected endpoint with a real token. AddAuthorization and RequireAuthorization() are the standard ASP.NET Core calls that protect /me.

Errors. AddProblemDetails, UseExceptionHandler and UseStatusCodePages turn other errors, such as an unknown route, into ProblemDetails responses.

Run and verify

Start the application:

dotnet run

The console shows where it listens:

[15:04:54 INF]            Microsoft.Hosting.Lifetime: Now listening on: http://localhost:5080
[15:04:54 INF]            Microsoft.Hosting.Lifetime: Application started. Press Ctrl+C to shut down.

In a second terminal, call the anonymous endpoint:

curl -i http://localhost:5080/hello/Ada
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8

Hello Ada, from GettingStarted (Local).

The name and environment come from Application.Configuration, and IGreetingService is resolved through the generated registration. The application console shows the entry written by logger.Business(), with its category and class:

[15:04:55 INF] Business   GettingStarted.Services.GreetingService: Greeting Ada

Now call the protected endpoint without a token:

curl -i http://localhost:5080/me
HTTP/1.1 401 Unauthorized
Content-Type: application/problem+json

{"status":401}

With a token that is not a valid JWT, the body also names the error:

curl -i -H "Authorization: Bearer not-a-jwt" http://localhost:5080/me
HTTP/1.1 401 Unauthorized
Content-Type: application/problem+json

{"title":"invalid_token","status":401,"detail":""}

and the application logs the reason with the Technical category:

[15:04:55 ERR] Technical  Arc4u.OAuth2.Events.StandardBearerEvents: Exception: IDX14100: JWT is not well formed, there are no dots (.).

You now have a running Arc4u application. Stop it with Ctrl+C.

Call the protected endpoint with a real token

To get a 200 from /me, point the application to your identity provider (Microsoft Entra ID, Keycloak, ADFS, or any OpenID Connect provider) and send one of its access tokens.

  1. Set Authentication:DefaultAuthority:Url to the authority URL of your identity provider. The application downloads the signing keys from <Url>/.well-known/openid-configuration; set Authentication:DefaultAuthority:MetaDataAddress if your provider publishes its metadata elsewhere. HTTPS is required to download the metadata, unless the metadata address itself starts with http://, which is only suitable for a local test provider.

  2. Set Authentication:OAuth2.Settings:Audiences to the value of the aud claim in the access tokens your provider issues for this API. For Microsoft Entra ID, it is the application (client) ID or the Application ID URI of the API registration, depending on the token version.

  3. Restart the application, get an access token from your provider (for example with the client credentials flow), and call:

    curl -H "Authorization: Bearer <access-token>" http://localhost:5080/me
    

    The response lists the claims of the token, for example:

    [{"type":"iss","value":"https://<authority>"},{"type":"aud","value":"getting-started-api"},{"type":"sub","value":"1234"},{"type":"name","value":"Ada Lovelace"},{"type":"iat","value":"1790679780"},{"type":"nbf","value":"1790679780"},{"type":"exp","value":"1790690580"}]
    

You can also override the settings with environment variables instead of editing appsettings.json (__ replaces :), for example in a Linux or macOS shell:

Authentication__DefaultAuthority__Url=https://<your-provider>/realms/<realm> dotnet run

With a token that is refused, /me still answers 401, and the detail field of the body gives the reason:

Token Response
Expired 401 with an x-token-expired response header, and detail "The token expired on ..."
Wrong audience 401 with detail "The audience '...' of the token is not an audience accepted by this service."
Caution

AddJwtAuthentication validates the signature, the lifetime, the audience and the issuer (iss claim) of a token. The issuer is checked against DefaultAuthority:Issuer when it is set, or against the issuer of the provider's metadata, and a token from another issuer gets 401 with detail "The issuer '...' is invalid". Do not turn it off with Authentication:ValidateAuthority: false: with providers whose tenants or realms share signing keys (Microsoft Entra ID, for example), a token issued by another tenant for the same audience would then be accepted.

Some providers, such as Keycloak with its default settings, issue access tokens without an audience for your API: add one on the provider side rather than turning audience validation off. The Server authentication guide covers the other settings.

Troubleshooting

Symptom Cause and fix
'IServiceCollection' does not contain a definition for 'RegisterGettingStartedTypes' The generator did not run or the name differs. Check that Arc4u.Dependency.Tool is referenced, and use the last part of your assembly name (Register<Name>Types).
InvalidOperationException: Bad Arc4u usage. AddILogger() is not called.
ConfigurationException: TokenCacheOptions.CacheName is not defined in the configuration file. The Authentication:TokenCache section exists without CacheName. This application makes no outgoing call, so remove the section.
ConfigurationException: Audiences field is not defined. Authentication:OAuth2.Settings:Audiences is missing or empty.
MissingFieldException: DefaultAuthority must be filled! Authentication:DefaultAuthority is missing.
InvalidOperationException: No section exists with name Authentication ... The Authentication section is missing.
ConfigurationException: Application environment time zone is not defined ... (or name, logging name) A value of Application.Configuration is missing.
ConfigurationException: The section 'Application.Configuration' does not exist or is empty. The Application.Configuration section is missing or misspelled.

Next steps

Each part of this application has a guide:

The other feature areas:

To understand the ideas behind Arc4u, read the Concepts.