diff --git a/src/Alba.sln b/src/Alba.sln
index 6fcbff5..37da546 100644
--- a/src/Alba.sln
+++ b/src/Alba.sln
@@ -22,6 +22,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IdentityServer.New", "Ident
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "..\build\_build.csproj", "{2D104BF1-7D1F-4DFF-9023-AEDF73C34B67}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Aspire", "Aspire", "{C65CC4CF-3D98-46FF-BF2D-D561AEBDD97C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspireApp.AppHost", "AspireApp\AspireApp.AppHost\AspireApp.AppHost.csproj", "{B66E4CB2-4F6B-4291-BC3A-44DF0474F04A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspireApp.ServiceDefaults", "AspireApp\AspireApp.ServiceDefaults\AspireApp.ServiceDefaults.csproj", "{5974619B-7169-4E63-A52F-3EF461B17509}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspireApp.ApiService", "AspireApp\AspireApp.ApiService\AspireApp.ApiService.csproj", "{A775660C-0BC4-4B12-AF9D-41E21D751244}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspireApp.Tests", "AspireApp\AspireApp.Tests\AspireApp.Tests.csproj", "{48552EF6-DE98-404F-B7C9-D6D85207ECE8}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -68,6 +78,22 @@ Global
{B7AB5313-B55C-4AFF-940B-F17DC913AFB6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B7AB5313-B55C-4AFF-940B-F17DC913AFB6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B7AB5313-B55C-4AFF-940B-F17DC913AFB6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B66E4CB2-4F6B-4291-BC3A-44DF0474F04A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B66E4CB2-4F6B-4291-BC3A-44DF0474F04A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B66E4CB2-4F6B-4291-BC3A-44DF0474F04A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B66E4CB2-4F6B-4291-BC3A-44DF0474F04A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5974619B-7169-4E63-A52F-3EF461B17509}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5974619B-7169-4E63-A52F-3EF461B17509}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5974619B-7169-4E63-A52F-3EF461B17509}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5974619B-7169-4E63-A52F-3EF461B17509}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A775660C-0BC4-4B12-AF9D-41E21D751244}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A775660C-0BC4-4B12-AF9D-41E21D751244}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A775660C-0BC4-4B12-AF9D-41E21D751244}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A775660C-0BC4-4B12-AF9D-41E21D751244}.Release|Any CPU.Build.0 = Release|Any CPU
+ {48552EF6-DE98-404F-B7C9-D6D85207ECE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {48552EF6-DE98-404F-B7C9-D6D85207ECE8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {48552EF6-DE98-404F-B7C9-D6D85207ECE8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {48552EF6-DE98-404F-B7C9-D6D85207ECE8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -75,4 +101,10 @@ Global
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2C4FAA2B-F991-4732-8D57-DFB1178440CA}
EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {B66E4CB2-4F6B-4291-BC3A-44DF0474F04A} = {C65CC4CF-3D98-46FF-BF2D-D561AEBDD97C}
+ {5974619B-7169-4E63-A52F-3EF461B17509} = {C65CC4CF-3D98-46FF-BF2D-D561AEBDD97C}
+ {A775660C-0BC4-4B12-AF9D-41E21D751244} = {C65CC4CF-3D98-46FF-BF2D-D561AEBDD97C}
+ {48552EF6-DE98-404F-B7C9-D6D85207ECE8} = {C65CC4CF-3D98-46FF-BF2D-D561AEBDD97C}
+ EndGlobalSection
EndGlobal
diff --git a/src/AspireApp/AspireApp.ApiService/AspireApp.ApiService.csproj b/src/AspireApp/AspireApp.ApiService/AspireApp.ApiService.csproj
new file mode 100644
index 0000000..1c0a354
--- /dev/null
+++ b/src/AspireApp/AspireApp.ApiService/AspireApp.ApiService.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AspireApp/AspireApp.ApiService/Program.cs b/src/AspireApp/AspireApp.ApiService/Program.cs
new file mode 100644
index 0000000..2c9ec9c
--- /dev/null
+++ b/src/AspireApp/AspireApp.ApiService/Program.cs
@@ -0,0 +1,54 @@
+using Marten;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Add service defaults & Aspire components.
+builder.AddServiceDefaults();
+
+// Add services to the container.
+builder.Services.AddProblemDetails();
+// calling this directly gives me the wrong extension for whatever reason
+AspirePostgreSqlNpgsqlExtensions.AddNpgsqlDataSource(builder,"apiservicedb");
+builder.Services.AddMarten(x =>
+{
+ x.CreateDatabasesForTenants(c=>
+ c.ForTenant()
+ .CheckAgainstPgDatabase()
+ );
+
+ x.UseSystemTextJsonForSerialization();
+
+}).ApplyAllDatabaseChangesOnStartup()
+ .UseNpgsqlDataSource();
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+app.UseExceptionHandler();
+
+app.MapGet("/weatherforecast", async (IQuerySession session) =>
+{
+ return await session.Query().ToListAsync();
+});
+
+app.MapPost("/weatherforcast", async (CreateWeatherForecastRequest request, IDocumentSession session) =>
+{
+ var forecast = new WeatherForecast(Guid.NewGuid(), request.Date, request.TemperatureC, request.Summary);
+
+ session.Store(forecast);
+
+ await session.SaveChangesAsync();
+});
+
+app.MapDefaultEndpoints();
+
+app.Run();
+
+
+public partial class Program {}
+public record CreateWeatherForecastRequest(DateOnly Date, int TemperatureC, string? Summary);
+
+public record WeatherForecast(Guid Id, DateOnly Date, int TemperatureC, string? Summary)
+{
+ public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
+}
\ No newline at end of file
diff --git a/src/AspireApp/AspireApp.ApiService/Properties/launchSettings.json b/src/AspireApp/AspireApp.ApiService/Properties/launchSettings.json
new file mode 100644
index 0000000..4cf9603
--- /dev/null
+++ b/src/AspireApp/AspireApp.ApiService/Properties/launchSettings.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "weatherforecast",
+ "applicationUrl": "http://localhost:5489",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "weatherforecast",
+ "applicationUrl": "https://localhost:7312;http://localhost:5489",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/src/AspireApp/AspireApp.ApiService/appsettings.Development.json b/src/AspireApp/AspireApp.ApiService/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/src/AspireApp/AspireApp.ApiService/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/src/AspireApp/AspireApp.ApiService/appsettings.json b/src/AspireApp/AspireApp.ApiService/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/src/AspireApp/AspireApp.ApiService/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/src/AspireApp/AspireApp.AppHost/AspireApp.AppHost.csproj b/src/AspireApp/AspireApp.AppHost/AspireApp.AppHost.csproj
new file mode 100644
index 0000000..0700a5d
--- /dev/null
+++ b/src/AspireApp/AspireApp.AppHost/AspireApp.AppHost.csproj
@@ -0,0 +1,21 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ true
+ 707F1E1C-F9AC-42D0-92BC-4CE87FD0EC4E
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AspireApp/AspireApp.AppHost/Program.cs b/src/AspireApp/AspireApp.AppHost/Program.cs
new file mode 100644
index 0000000..9e18d91
--- /dev/null
+++ b/src/AspireApp/AspireApp.AppHost/Program.cs
@@ -0,0 +1,9 @@
+var builder = DistributedApplication.CreateBuilder(args);
+
+var postgres = builder.AddPostgres("postgresdb").AddDatabase("apiservicedb");
+
+builder
+ .AddProject("apiservice")
+ .WithReference(postgres);
+
+builder.Build().Run();
\ No newline at end of file
diff --git a/src/AspireApp/AspireApp.AppHost/Properties/launchSettings.json b/src/AspireApp/AspireApp.AppHost/Properties/launchSettings.json
new file mode 100644
index 0000000..900b325
--- /dev/null
+++ b/src/AspireApp/AspireApp.AppHost/Properties/launchSettings.json
@@ -0,0 +1,29 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:17083;http://localhost:15041",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21024",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22268"
+ }
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15041",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19152",
+ "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20090"
+ }
+ }
+ }
+}
diff --git a/src/AspireApp/AspireApp.AppHost/appsettings.Development.json b/src/AspireApp/AspireApp.AppHost/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/src/AspireApp/AspireApp.AppHost/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/src/AspireApp/AspireApp.AppHost/appsettings.json b/src/AspireApp/AspireApp.AppHost/appsettings.json
new file mode 100644
index 0000000..31c092a
--- /dev/null
+++ b/src/AspireApp/AspireApp.AppHost/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Aspire.Hosting.Dcp": "Warning"
+ }
+ }
+}
diff --git a/src/AspireApp/AspireApp.ServiceDefaults/AspireApp.ServiceDefaults.csproj b/src/AspireApp/AspireApp.ServiceDefaults/AspireApp.ServiceDefaults.csproj
new file mode 100644
index 0000000..0bdf402
--- /dev/null
+++ b/src/AspireApp/AspireApp.ServiceDefaults/AspireApp.ServiceDefaults.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AspireApp/AspireApp.ServiceDefaults/Extensions.cs b/src/AspireApp/AspireApp.ServiceDefaults/Extensions.cs
new file mode 100644
index 0000000..0a2ee48
--- /dev/null
+++ b/src/AspireApp/AspireApp.ServiceDefaults/Extensions.cs
@@ -0,0 +1,111 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Diagnostics.HealthChecks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Logging;
+using OpenTelemetry;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Trace;
+
+namespace Microsoft.Extensions.Hosting;
+
+// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry.
+// This project should be referenced by each service project in your solution.
+// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults
+public static class Extensions
+{
+ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
+ {
+ builder.ConfigureOpenTelemetry();
+
+ builder.AddDefaultHealthChecks();
+
+ builder.Services.AddServiceDiscovery();
+
+ builder.Services.ConfigureHttpClientDefaults(http =>
+ {
+ // Turn on resilience by default
+ http.AddStandardResilienceHandler();
+
+ // Turn on service discovery by default
+ http.AddServiceDiscovery();
+ });
+
+ return builder;
+ }
+
+ public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder)
+ {
+ builder.Logging.AddOpenTelemetry(logging =>
+ {
+ logging.IncludeFormattedMessage = true;
+ logging.IncludeScopes = true;
+ });
+
+ builder.Services.AddOpenTelemetry()
+ .WithMetrics(metrics =>
+ {
+ metrics.AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation()
+ .AddRuntimeInstrumentation();
+ })
+ .WithTracing(tracing =>
+ {
+ tracing.AddAspNetCoreInstrumentation()
+ // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
+ //.AddGrpcClientInstrumentation()
+ .AddHttpClientInstrumentation();
+ });
+
+ builder.AddOpenTelemetryExporters();
+
+ return builder;
+ }
+
+ private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder)
+ {
+ var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
+
+ if (useOtlpExporter)
+ {
+ builder.Services.AddOpenTelemetry().UseOtlpExporter();
+ }
+
+ // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
+ //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
+ //{
+ // builder.Services.AddOpenTelemetry()
+ // .UseAzureMonitor();
+ //}
+
+ return builder;
+ }
+
+ public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder)
+ {
+ builder.Services.AddHealthChecks()
+ // Add a default liveness check to ensure app is responsive
+ .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
+
+ return builder;
+ }
+
+ public static WebApplication MapDefaultEndpoints(this WebApplication app)
+ {
+ // Adding health checks endpoints to applications in non-development environments has security implications.
+ // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.
+ if (app.Environment.IsDevelopment())
+ {
+ // All health checks must pass for app to be considered ready to accept traffic after starting
+ app.MapHealthChecks("/health");
+
+ // Only health checks tagged with the "live" tag must pass for app to be considered alive
+ app.MapHealthChecks("/alive", new HealthCheckOptions
+ {
+ Predicate = r => r.Tags.Contains("live")
+ });
+ }
+
+ return app;
+ }
+}
\ No newline at end of file
diff --git a/src/AspireApp/AspireApp.Tests/AspireApp.Tests.csproj b/src/AspireApp/AspireApp.Tests/AspireApp.Tests.csproj
new file mode 100644
index 0000000..de51a3f
--- /dev/null
+++ b/src/AspireApp/AspireApp.Tests/AspireApp.Tests.csproj
@@ -0,0 +1,30 @@
+
+
+
+ net8.0
+ enable
+ enable
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AspireApp/AspireApp.Tests/IntegrationTest.cs b/src/AspireApp/AspireApp.Tests/IntegrationTest.cs
new file mode 100644
index 0000000..6757ad0
--- /dev/null
+++ b/src/AspireApp/AspireApp.Tests/IntegrationTest.cs
@@ -0,0 +1,88 @@
+using Alba;
+using Marten;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace AspireApp.Tests;
+
+[Collection(nameof(WebAppCollection))]
+public class IntegrationTest
+{
+ private readonly IAlbaHost _alba;
+
+ public IntegrationTest(WebAppFixture fixture)
+ {
+ _alba = fixture.Alba;
+ }
+
+ [Fact]
+ public async Task CanCreateWeatherForecast()
+ {
+ var forecast = new CreateWeatherForecastRequest(DateOnly.FromDateTime(DateTime.Now), 23, "etc");
+ await _alba.Scenario(x =>
+ {
+ x.Post.Json(forecast)
+ .ToUrl("weatherforcast");
+ x.StatusCodeShouldBeOk();
+ });
+
+ var forecasts = await _alba.GetAsJson("weatherforcast");
+
+ Assert.Single(forecasts!);
+ }
+}
+
+
+[CollectionDefinition(nameof(WebAppCollection))]
+public class WebAppCollection : ICollectionFixture
+{
+}
+
+
+public sealed class WebAppFixture : IAsyncLifetime
+{
+ public IAlbaHost Alba = null!;
+ private IHost _aspireHost = null!;
+
+ public async Task DisposeAsync()
+ {
+ await Alba.Services.GetRequiredService().Advanced.Clean.DeleteAllDocumentsAsync();
+
+ await Alba.DisposeAsync();
+ if (_aspireHost is IAsyncDisposable asyncDisposable)
+ {
+ await asyncDisposable.DisposeAsync().ConfigureAwait(false);
+ }
+ else
+ {
+ _aspireHost.Dispose();
+ }
+ }
+
+ public async Task InitializeAsync()
+ {
+ var appHost = await DistributedApplicationTestingBuilder.CreateAsync();
+ appHost.Resources.Remove(appHost.Resources.Single(r => r.Name == "webfrontend"));
+ appHost.Resources.Remove(appHost.Resources.Single(r => r.Name == "apiservice"));
+ var postgres = (IResourceWithConnectionString)appHost.Resources.Single(x => x.Name == "postgres");
+ _aspireHost = await appHost.BuildAsync();
+ await _aspireHost.StartAsync();
+
+ var connectionString = await postgres.GetConnectionStringAsync();
+ Alba = await AlbaHost.For(x =>
+ {
+ x.ConfigureAppConfiguration((context, builder) =>
+ {
+ builder.AddInMemoryCollection(new Dictionary
+ {
+ { $"ConnectionStrings:{postgres.Name}", connectionString },
+ });
+ });
+ });
+ }
+}
+
+
+
+