Aspire-AppHost¶
Coworkee.Aspire macht aus dem AppHost wenige Zeilen. Sie nennen die App; Coworkee findet ihre Dienste, ergänzt die Infrastruktur, die jeder braucht, und die Einstellungen, die sie verbinden.
var builder = DistributedApplication.CreateBuilder(args);
builder.AddCoworkeeApp("myapp", options =>
{
options.DisplayName = "MyApp";
options.LogoUrl = "/coworkee-icon.svg";
options.UseKeycloak(keycloak => keycloak.Users.Add(new KeycloakUser("info@coworkee.de", "Administrator", "MyApp") { Password = "Dev-Passw0rd!" }));
}).AddProjects();
builder.Build().Run();
LogoUrl ist das Logo der Anmeldeseiten, solange das Theme keines hat: Ein Pfad wird auf der Web-App genommen ({web}/coworkee-icon.svg), eine absolute Adresse so, wie sie ist.
options.SignIn setzt Layout, Medium und Seite des Mediums der Anmeldeseiten (Coworkee:Auth:Design des Auth-Servers); ein Pfad zum Medium wird wie LogoUrl auf der Web-App aufgelöst:
options.SignIn.Layout = SignInLayouts.Split;
options.SignIn.MediaUrl = "/media/welcome.mp4";
options.SignIn.MediaSide = SignInMediaSide.Right;
Sharemee ergänzt danach einen Worker, dessen Name keiner Konvention folgt:
var app = builder.AddCoworkeeApp("shareme", ...).AddProjects();
app.AddWorker<Projects.Shareme_Processing_Host>("processing");
Projekte nach Konvention¶
AddProjects() schaut sich die Projects.*-Typen an, die Aspire für die referenzierten Projekte des AppHosts erzeugt, und fügt sie nach Namen in dieser Reihenfolge hinzu:
| Typname | Hinzugefügt mit | Ressource |
|---|---|---|
*_Migrations |
AddMigrations |
{name}-migrations |
*_Auth |
AddAuthServer |
{name}-auth |
*_Api |
AddApi |
{name}-api |
*_Worker*, *Worker |
AddWorker |
{name}-worker |
*_Web |
AddWeb |
{name}-web |
Das Suffix ist der Teil nach dem ersten Unterstrich, kleingeschrieben mit Bindestrichen: Aus MyApp_Jobs_Worker wird myapp-jobs-worker. Andere Projekte bleiben unberührt.
builder.AddCoworkeeApp("myapp", options =>
{
options.Configure("api", api => api.WithReplicas(2)); // nach Suffix, auch für explizit hinzugefügte Dienste
options.Skip("web", "MyApp_Admin_Api"); // nach Suffix oder Typname
}).AddProjects();
Explizite Aufrufe funktionieren weiter und haben Vorrang: Ein Projekt, das vor AddProjects() hinzugefügt wurde, kommt nicht doppelt hinzu. Soll ein gefundenes Projekt eine andere Rolle oder ein anderes Suffix bekommen, überspringen Sie es und fügen es danach selbst hinzu.
Rollen¶
| Methode | Ressource | Verdrahtung |
|---|---|---|
AddMigrations<T>() |
{name}-migrations |
Datenbank, wartet auf den Datenbankserver, Dashboard-Befehle |
AddAuthServer<T>() |
{name}-auth |
externe Endpunkte, kein Job-Server, öffentliche Auth-URL, Entwicklungszertifikate in Development, Keycloak-Anbieter |
AddApi<T>() |
{name}-api |
Token-Authority und Audience {name}_api, API-Scope am Auth-Server, Setup-Token aus Coworkee:SetupToken |
AddWorker<T>(suffix) |
{name}-{suffix} |
nur Infrastruktur, keine öffentlichen Endpunkte |
AddWeb<T>() |
{name}-web |
BFF-Einstellungen, OIDC-Client am Auth-Server mit Redirect-URIs, Referenz auf die API, Links zu Swagger und zum Job-Dashboard |
Rufen Sie sie in dieser Reihenfolge auf (AddProjects() tut das). AddWeb vor AddAuthServer wirft eine Exception, die genau das sagt.
Infrastruktur aus den Paketen¶
Bevor Coworkee einen Dienst verdrahtet, liest es dessen obj/project.assets.json und schaut, welche Pakete und Projekte er nutzt, direkt oder über andere Projekte:
| Paket im Dienst | Er bekommt |
|---|---|
Coworkee.Infrastructure |
Connection-String der App-Datenbank und Coworkee:Database:Provider, wartet, bis die Migrationen fertig sind |
Coworkee.Realtime |
Redis für die SignalR-Backplane, Anwesenheit, Cache-Invalidierung und Sitzungsänderungen zwischen Instanzen (Skalieren mit Redis); options.WithoutRedis() lässt es weg |
Coworkee.Mailing |
Mailpit als Standard-SMTP-Server |
Coworkee.Storage |
den gemeinsamen Blob-Ordner .data/blobs (nur lokal; veröffentlichte Container behalten ihren Standard) |
Coworkee.Search.Elasticsearch |
Elasticsearch |
Coworkee.Notifications |
Links in Zusammenfassungsmails zeigen auf die Web-App |
Grafana (mit Prometheus, Loki, Tempo und Collector) und WebDataStudio kommen mit options.UseGrafana() und options.UseWebDataStudio() dazu, siehe Grafana und WebDataStudio.
Container entstehen nur, wenn ein Dienst sie braucht. Postgres, SQL Server, Redis, Elasticsearch und Keycloak halten ihre Daten in Volumes; --Coworkee:EphemeralInfrastructure=true startet sie leer, das nutzen die Tests.
Eigene Module¶
app.Modules enthält diese Verdrahtung nach Paket- oder Projektname. App-Module melden ihren Bedarf genauso an; jeder Dienst, der das Projekt direkt oder transitiv referenziert, bekommt ihn:
var app = builder.AddCoworkeeApp("myapp");
var stripe = builder.AddConnectionString("stripe");
app.Modules.Add("MyApp.Billing", (app, service) => service.WithReference(stripe));
app.AddProjects();
Registrieren Sie Module, bevor Sie die Dienste hinzufügen. app.Modules[CoworkeeModules.Search] = ... ersetzt eine eingebaute Verdrahtung, app.Modules.Remove(...) entfernt sie.
Eigene Infrastruktur¶
Jedes Teil lässt sich konfigurieren, ersetzen oder abschalten:
builder.AddCoworkeeApp("myapp", options =>
{
options.ConfigurePostgres(postgres => postgres.WithPgAdmin())
.ConfigureRedis(redis => redis.WithRedisInsight())
.ConfigureMail(mail => mail.WithLifetime(ContainerLifetime.Persistent))
.ConfigureSearch(search => search.WithEnvironment("ES_JAVA_OPTS", "-Xmx1g"))
.ConfigureKeycloak(keycloak => keycloak.WithLifetime(ContainerLifetime.Persistent));
// Dienste mit Coworkee.Search.Elasticsearch bekommen kein Elasticsearch
options.Without(CoworkeeModules.Search);
}).AddProjects();
| Option | Statt |
|---|---|
UseDatabase(resource) |
des Datenbank-Containers; jede Ressource mit Connection-String, z. B. builder.AddConnectionString("myapp") |
UseSqlServer(configure) |
von Postgres: ein SQL-Server-Container (app.SqlServer), oder mit UseDatabase ein eigener SQL Server |
UseSqlite(path) |
von Postgres: eine SQLite-Datei, ohne Angabe .data/<name>.db neben src; kein Container |
UseRedis(resource) |
des Redis-Containers |
UseSearch(resource) |
des Elasticsearch-Containers |
UseMail(endpoint) |
von Mailpit; der SMTP-Endpunkt einer anderen Ressource |
UseKeycloak(resource, ...) |
des Keycloak-Containers; Ihr Keycloak bekommt den Realm |
UseSqlServer und UseSqlite setzen bei den Diensten auch Coworkee:Database:Provider; die Migrationen der App müssen für diesen Provider erstellt sein (siehe Datenbank-Provider).
Die Dienste bekommen eigene Ressourcen unter den Namen, die sie erwarten (ConnectionStrings:myapp, redis, elasticsearch). Coworkee wartet nicht auf sie; ergänzen Sie bei Bedarf WaitFor über options.Configure.
// Produktion: der Connection-String kommt aus der Konfiguration (ConnectionStrings:myapp) oder wird beim Deployment abgefragt
var database = builder.AddConnectionString("myapp");
builder.AddCoworkeeApp("myapp", options => options.UseDatabase(database)).AddProjects();
Dashboard¶
Läuft der AppHost lokal in Development, hat die Migrations-Ressource zwei Befehle:
- Re-run migrations startet das Migrationsprojekt erneut: neue Migrationen und das Seeding der Demodaten.
- Reset database löscht nach einer Rückfrage die App-Datenbank und führt die Migrationen erneut aus. Den Befehl gibt es nur für den eingebauten Postgres-Container, nie für SQL Server, SQLite oder eine eigene Datenbank und nie außerhalb von Development.
Die Web-Ressource verlinkt Swagger (/swagger) und, wenn eine API Coworkee.BackgroundJobs nutzt, das Hangfire-Dashboard (Jobs, /admin/jobs). Mailpit zeigt seinen Posteingang als Mailpit UI.
Dienstadressen¶
Jeder Dienst der App bekommt die Adressen der anderen unter Coworkee:Services:<Ressource>: das Aspire-Dashboard (dashboard), das Hangfire-Dashboard (jobs), jedes Projekt (myapp-api, myapp-auth, myapp-web, mit HealthPath /health) und jeden Container mit http-Endpunkt (mail, pgadmin, keycloak, ...), auch Ressourcen, die nach AddCoworkeeApp hinzukommen. Injiziert werden sie als IOptionsMonitor<CoworkeeServicesOptions>; die Admin-Seite Dienste zeigt sie mit ihrem aktuellen Zustand (siehe Einstellungen). Nur der lokale Lauf füllt sie; eine veröffentlichte App trägt ihre öffentlichen Adressen in appsettings ein.
Typisierte Einstellungen¶
Einstellungen werden über den Konfigurationsbaum gesetzt, nicht über Zeichenketten:
api.WithSetting(s => s.Coworkee.Storage.Provider, "S3")
.WithSetting(s => s.Coworkee.Settings.Defaults["Mail.Smtp.Host"], "smtp.example.com")
.WithSettings(s => s.Coworkee.Bff.ForwardedPrefixes, "/admin/jobs", "/hubs");
Aus s => s.Coworkee.Bff.Scopes[0] wird die Umgebungsvariable Coworkee__Bff__Scopes__0. Werte können Zeichenketten, Zahlen, Wahrheitswerte, Endpunkt-Referenzen, Referenzausdrücke oder Parameter sein. CoworkeeSettings spiegelt die echten Options-Klassen aus Coworkee.Contracts.Configuration; eine umbenannte Eigenschaft bricht also den Build des AppHosts, nicht die laufende App.
Keycloak¶
UseKeycloak fügt einen Keycloak-Container mit einem Realm hinzu, der wie die App heißt:
- ein vertraulicher Client
{name}-auth, dessen Secret ein generierter Parameter ist ({name}-keycloak-client-secret), - die angegebenen Benutzer, jeder mit seinem
Password; Benutzer ohne teilen sich ein generiertes Passwort ({name}-keycloak-user-password, im Dashboard sichtbar und in den User-Secrets des AppHosts gespeichert).
Der Realm wird bei jedem Start auf den aktuellen Stand gebracht, sobald Keycloak bereit ist und bevor der Auth-Server startet: Ein fehlender Realm wird angelegt, ein vorhandener (im Daten-Volume) bekommt über die Admin-API die aktuellen Client-Secrets, Redirect-Adressen, Benutzer und Passwörter. Ein geänderter Secret-Parameter oder ein geändertes Passwort gilt also ab dem nächsten Start, und Benutzer oder Clients, die Sie in der Keycloak-Konsole angelegt haben, bleiben erhalten. Das Ressourcen-Log von Keycloak meldet Realm myapp is up to date: clients ..., users ....
Der Auth-Server bekommt die Anbieter-Einstellungen und bietet Sign in with Keycloak an. LoginMode schaltet zwischen beidem, nur extern und nur intern um; Port legt den Host-Port fest, wenn Sie eine feste URL wollen.
Browser schicken die Cookies von localhost an jeden Port, also erreichen die Cookies aller lokalen Apps auch Keycloak. Über HTTPS spricht Keycloak HTTP/2 und nimmt dabei standardmäßig nur 8 KB Request-Header an; darüber antwortet es mit 431 Request Header Fields Too Large. Der Container bekommt deshalb QUARKUS_HTTP_LIMITS_MAX_HEADER_LIST_SIZE=65536, die 64 KB, die er über HTTP/1.1 annimmt. Zeigt der Browser trotzdem 431, löschen Sie die localhost-Cookies anderer Apps; Auth-Server und Web-App (Kestrel) nehmen 32 KB an.
Weitere Clients und die Benutzer¶
Andere Ressourcen des AppHosts können sich über denselben Realm anmelden und seine Benutzer wiederverwenden, zum Beispiel ein Dashboard mit OpenID Connect:
var app = builder.AddCoworkeeApp("myapp", options => options.UseKeycloak(k => k.Users.AddRange(users))).AddProjects();
var grafana = builder.AddContainer("grafana", "grafana/grafana").WithHttpEndpoint(targetPort: 3000);
var client = app.AddKeycloakClient("myapp-grafana", [ReferenceExpression.Create($"{grafana.GetEndpoint("http")}/login/generic_oauth")]);
grafana.WithEnvironment("GF_AUTH_GENERIC_OAUTH_CLIENT_ID", client.ClientId)
.WithEnvironment("GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET", client.Secret)
.WithEnvironment("GF_AUTH_GENERIC_OAUTH_AUTH_URL", ReferenceExpression.Create($"{app.KeycloakAuthority}/protocol/openid-connect/auth"));
Member von CoworkeeApp |
|
|---|---|
AddKeycloakClient(clientId, redirectUris?, secret?) |
ein weiterer vertraulicher Client des Realms; ohne Secret ein generiertes ({clientId}-secret), ohne Redirect-Adressen jede lokale Adresse |
KeycloakClients |
die Clients des Realms, der des Auth-Servers zuerst |
KeycloakAuthority |
Issuer-Adresse des Realms, {keycloak}/realms/{realm} |
KeycloakUsers, KeycloakPassword(user) |
die Benutzer und das Passwort jedes Benutzers als Reference Expression, etwa für den lokalen Login einer anderen Ressource |
Rufen Sie sie nach UseKeycloak und AddAuthServer (oder AddProjects) auf.
Veröffentlichen¶
PublishToDockerCompose() fügt eine Docker-Compose-Umgebung hinzu. aspire publish schreibt dann eine docker-compose.yaml und eine .env-Datei mit allen Diensten, Containern, Volumes und der Startreihenfolge (Dienste starten, sobald die Migrationen abgeschlossen sind):
builder.AddCoworkeeApp("myapp", options => options.PublishToDockerCompose(compose => compose.WithDashboard(false)))
.AddProjects();
Azure Container Apps funktioniert über die austauschbare Infrastruktur; nehmen Sie Aspire.Hosting.Azure.AppContainers und Aspire.Hosting.Azure.PostgreSQL in den AppHost auf:
builder.AddAzureContainerAppEnvironment("aca");
var database = builder.AddAzurePostgresFlexibleServer("postgres").RunAsContainer().AddDatabase("myapp");
builder.AddCoworkeeApp("myapp", options => options.UseDatabase(database)).AddProjects();
Produktion¶
Der AppHost beschreibt die Entwicklung. Für die Produktion konfigurieren Sie zusätzlich echte Zertifikate (Coworkee:Auth:SigningCertificate, EncryptionCertificate), die öffentlichen URLs hinter Ihrem Reverse Proxy, einen echten SMTP-Server, Speicher und Keycloak. Das Dashboard eines lokalen Laufs listet die Einstellungen, die jeder Dienst braucht.