diff --git a/.aspire/settings.json b/.aspire/settings.json index 3c52ba2..22d27ee 100644 --- a/.aspire/settings.json +++ b/.aspire/settings.json @@ -1,3 +1,3 @@ { - "appHostPath": "../AndreGoepel.MembersArea/AndreGoepel.MembersArea.AppHost/AndreGoepel.MembersArea.AppHost.csproj" -} \ No newline at end of file + "appHostPath": "samples/AndreGoepel.AppFoundation.AppHost/AndreGoepel.AppFoundation.AppHost.csproj" +} diff --git a/AndreGoepel.AppFoundation.slnx b/AndreGoepel.AppFoundation.slnx index 94f341c..606a5d2 100644 --- a/AndreGoepel.AppFoundation.slnx +++ b/AndreGoepel.AppFoundation.slnx @@ -14,4 +14,8 @@ + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props index f19a2e1..47b81c2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,8 +4,8 @@ - - + + diff --git a/README.md b/README.md index fd1f554..7c9a49a 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,30 @@ for a complete, working example. --- +## Try it locally (sample host) + +The repo ships a runnable example under `samples/`: a .NET Aspire AppHost that starts +PostgreSQL in a container plus a minimal Blazor Server host that wires the packages exactly +as [Using it in a host app](#using-it-in-a-host-app) describes. It doubles as a manual smoke +test for the foundation. + +**Prerequisites:** the .NET 10 SDK and a container runtime (Docker / Podman) for the database. + +```bash +dotnet run --project samples/AndreGoepel.AppFoundation.AppHost +``` + +Open the Aspire dashboard URL printed to the console, start the **web** resource, and open it. +The first visit funnels you to **/Setup** to create the administrator; after that you can sign +in and explore the dashboard and the Administration area. + +| Project | Role | +|---|---| +| `samples/AndreGoepel.AppFoundation.AppHost` | Aspire orchestrator — starts Postgres and the web app, wiring the `appfoundation-database` connection string | +| `samples/AndreGoepel.AppFoundation.Sample` | Minimal Blazor Server host consuming the packages — the reference `Program.cs`, `App.razor`, and `Routes.razor` | + +--- + ## Configuration A PostgreSQL connection string is required, by default under diff --git a/samples/AndreGoepel.AppFoundation.AppHost/AndreGoepel.AppFoundation.AppHost.csproj b/samples/AndreGoepel.AppFoundation.AppHost/AndreGoepel.AppFoundation.AppHost.csproj new file mode 100644 index 0000000..55bef25 --- /dev/null +++ b/samples/AndreGoepel.AppFoundation.AppHost/AndreGoepel.AppFoundation.AppHost.csproj @@ -0,0 +1,28 @@ + + + + + Exe + net10.0 + enable + enable + false + + false + + false + appfoundation-sample-apphost + + + + + + + + + + + diff --git a/samples/AndreGoepel.AppFoundation.AppHost/AppHost.cs b/samples/AndreGoepel.AppFoundation.AppHost/AppHost.cs new file mode 100644 index 0000000..95eecce --- /dev/null +++ b/samples/AndreGoepel.AppFoundation.AppHost/AppHost.cs @@ -0,0 +1,16 @@ +var builder = DistributedApplication.CreateBuilder(args); + +// A PostgreSQL container with a persistent volume so setup and accounts survive restarts. +var postgres = builder.AddPostgres("postgres").WithDataVolume(); + +// The database resource name is the connection-string name the foundation reads by default +// (AppFoundationOptions.DatabaseConnectionName == "appfoundation-database"). +var database = postgres.AddDatabase("appfoundation-database", "appfoundation"); + +// The sample web app, wired to the database and started only once it is ready. +builder + .AddProject("web") + .WithReference(database) + .WaitFor(database); + +builder.Build().Run(); diff --git a/samples/AndreGoepel.AppFoundation.AppHost/Properties/launchSettings.json b/samples/AndreGoepel.AppFoundation.AppHost/Properties/launchSettings.json new file mode 100644 index 0000000..9abfffd --- /dev/null +++ b/samples/AndreGoepel.AppFoundation.AppHost/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:17010;http://localhost:15010", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development" + } + }, + "http": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "http://localhost:15010", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/AndreGoepel.AppFoundation.AppHost/appsettings.json b/samples/AndreGoepel.AppFoundation.AppHost/appsettings.json new file mode 100644 index 0000000..31c092a --- /dev/null +++ b/samples/AndreGoepel.AppFoundation.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/samples/AndreGoepel.AppFoundation.Sample/AndreGoepel.AppFoundation.Sample.csproj b/samples/AndreGoepel.AppFoundation.Sample/AndreGoepel.AppFoundation.Sample.csproj new file mode 100644 index 0000000..f4fe9d3 --- /dev/null +++ b/samples/AndreGoepel.AppFoundation.Sample/AndreGoepel.AppFoundation.Sample.csproj @@ -0,0 +1,22 @@ + + + net10.0 + enable + enable + false + + false + + + + + + + + + + + diff --git a/samples/AndreGoepel.AppFoundation.Sample/Components/App.razor b/samples/AndreGoepel.AppFoundation.Sample/Components/App.razor new file mode 100644 index 0000000..10a3fb2 --- /dev/null +++ b/samples/AndreGoepel.AppFoundation.Sample/Components/App.razor @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/AndreGoepel.AppFoundation.Sample/Components/Pages/Home.razor b/samples/AndreGoepel.AppFoundation.Sample/Components/Pages/Home.razor new file mode 100644 index 0000000..7013945 --- /dev/null +++ b/samples/AndreGoepel.AppFoundation.Sample/Components/Pages/Home.razor @@ -0,0 +1,48 @@ +@page "/" +@rendermode InteractiveServer + +@using AndreGoepel.Marten.Identity +@using global::Marten + +@inject IQuerySession QuerySession +@inject NavigationManager NavigationManager + +AppFoundation Sample + +
+ + + AppFoundation Sample + + + A minimal host that wires the AppFoundation packages. Use it to exercise the setup, + sign-in, and administration flows against a real PostgreSQL started by .NET Aspire. + + + + @if (setupComplete) + { + + + } + else + { + + } + + + +
+ +@code { + private bool setupComplete; + + protected override async Task OnInitializedAsync() => + setupComplete = await SetupCompletion.IsCompleteAsync(QuerySession); +} diff --git a/samples/AndreGoepel.AppFoundation.Sample/Components/Routes.razor b/samples/AndreGoepel.AppFoundation.Sample/Components/Routes.razor new file mode 100644 index 0000000..177dd40 --- /dev/null +++ b/samples/AndreGoepel.AppFoundation.Sample/Components/Routes.razor @@ -0,0 +1,23 @@ +@using AndreGoepel.AppFoundation.Components.Layout +@using AndreGoepel.Marten.Identity.Blazor.Components.Account.Shared + + + + + + + + + + + + +@code { + // The routable pages live in the AppFoundation UI and the Marten.Identity account + // packages, so the router must scan those assemblies in addition to this host. + private readonly System.Reflection.Assembly[] _additionalAssemblies = + [ + typeof(AppFoundationLayoutOptions).Assembly, + typeof(AndreGoepel.Marten.Identity.Blazor.Initialization).Assembly, + ]; +} diff --git a/samples/AndreGoepel.AppFoundation.Sample/Components/_Imports.razor b/samples/AndreGoepel.AppFoundation.Sample/Components/_Imports.razor new file mode 100644 index 0000000..dc86ac0 --- /dev/null +++ b/samples/AndreGoepel.AppFoundation.Sample/Components/_Imports.razor @@ -0,0 +1,16 @@ +@using System.Net.Http +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@using AndreGoepel.AppFoundation +@using AndreGoepel.AppFoundation.Components.Layout +@using AndreGoepel.AppFoundation.Components.Shared +@using AndreGoepel.Marten.Identity.Blazor.Components.Account.Shared +@using AndreGoepel.AppFoundation.Sample +@using AndreGoepel.AppFoundation.Sample.Components +@using Radzen +@using Radzen.Blazor diff --git a/samples/AndreGoepel.AppFoundation.Sample/Program.cs b/samples/AndreGoepel.AppFoundation.Sample/Program.cs new file mode 100644 index 0000000..9ca5921 --- /dev/null +++ b/samples/AndreGoepel.AppFoundation.Sample/Program.cs @@ -0,0 +1,47 @@ +using AndreGoepel.AppFoundation; +using AndreGoepel.AppFoundation.Hosting; +using AndreGoepel.AppFoundation.Sample.Components; +using AndreGoepel.Marten.Identity.Blazor.Components.Account; + +var builder = WebApplication.CreateBuilder(args); + +// One call wires the data store, identity, messaging, email, data protection, and the +// shared request pipeline. The connection string named "appfoundation-database" is +// supplied by the Aspire AppHost (or Docker secrets in production). +builder.AddAppFoundation(); + +builder.Services.AddRazorComponents().AddInteractiveServerComponents(); + +// Brand the management shell. A real host would also set LogoPath and, optionally, an +// AdminMenu component to contribute its own administration entries. +builder.Services.Configure(options => +{ + options.BrandName = "AppFoundation Sample"; + options.Copyright = $"© {DateTime.UtcNow:yyyy} AppFoundation"; + // A self-contained placeholder logo so the sample needs no binary asset. A real host + // points LogoPath at its own image under wwwroot. + options.LogoPath = + "data:image/svg+xml;utf8," + + "" + + "" + + "A"; +}); + +var app = builder.Build(); + +app.UseAppFoundation(); + +app.MapStaticAssets(); +app.MapRazorComponents() + .AddInteractiveServerRenderMode() + .AddAdditionalAssemblies( + // The AppFoundation management UI (layout, setup, dashboard, admin pages) … + typeof(AppFoundationLayoutOptions).Assembly, + // … and the Marten.Identity account pages (login, register, account management). + typeof(AndreGoepel.Marten.Identity.Blazor.Initialization).Assembly + ); + +app.MapAdditionalIdentityEndpoints(); + +app.Run(); diff --git a/samples/AndreGoepel.AppFoundation.Sample/Properties/launchSettings.json b/samples/AndreGoepel.AppFoundation.Sample/Properties/launchSettings.json new file mode 100644 index 0000000..183c528 --- /dev/null +++ b/samples/AndreGoepel.AppFoundation.Sample/Properties/launchSettings.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:7080;http://localhost:5080", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "http": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "http://localhost:5080", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/AndreGoepel.AppFoundation.Sample/appsettings.json b/samples/AndreGoepel.AppFoundation.Sample/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/samples/AndreGoepel.AppFoundation.Sample/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/AndreGoepel.AppFoundation.Sample/wwwroot/app.css b/samples/AndreGoepel.AppFoundation.Sample/wwwroot/app.css new file mode 100644 index 0000000..88a09c3 --- /dev/null +++ b/samples/AndreGoepel.AppFoundation.Sample/wwwroot/app.css @@ -0,0 +1,22 @@ +html, body { + margin: 0; + font-family: var(--rz-text-font-family, system-ui, sans-serif); +} + +#blazor-error-ui { + display: none; + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 0.75rem 1.25rem; + background: #ffe4e4; + color: #7a0000; + box-shadow: 0 -1px 4px rgba(0, 0, 0, 0.15); + z-index: 1000; +} + +#blazor-error-ui .dismiss { + cursor: pointer; + float: right; +}