Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions Hosuto.sln

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions samples/dotnet/minimal/App/App.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<RootNamespace>Dbosoft.Hosuto.Samples.Minimal.App</RootNamespace>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\RazorModule\RazorModule.csproj" />
</ItemGroup>

</Project>
58 changes: 58 additions & 0 deletions samples/dotnet/minimal/App/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Dbosoft.Hosuto.Modules.Hosting;
using Dbosoft.Hosuto.Samples.Minimal.RazorModule;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.Extensions.DependencyInjection;
using SimpleInjector;

namespace Dbosoft.Hosuto.Samples.Minimal.App
{
public static class Program
{
public static async Task Main()
{
var builder = ModulesHost.CreateDefaultBuilder();
builder.UseSimpleInjector(new Container());
builder.UseAspNetCoreMinimal(app => app.WebHost.UseUrls("http://127.0.0.1:0"));
builder.HostModule<RazorGreetModule>();

using var host = builder.Build();
await host.StartAsync();

var moduleHost = host.Services.GetRequiredService<IModuleHost<RazorGreetModule>>();
var baseUrl = moduleHost.Services.GetRequiredService<IServer>()
.Features.Get<IServerAddressesFeature>().Addresses.First();

using (var http = new HttpClient())
{
await Probe(http, baseUrl + "/", "Razor page");
await Probe(http, baseUrl + "/css/site.css", "static asset (wwwroot)");
}

await host.StopAsync();
}

private static async Task Probe(HttpClient http, string url, string what)
{
try
{
var response = await http.GetAsync(url);
var body = await response.Content.ReadAsStringAsync();
var snippet = body.Replace("\r", " ").Replace("\n", " ").Trim();
if (snippet.Length > 70) snippet = snippet.Substring(0, 70) + "...";
Console.WriteLine($"[{what,-22}] {(int)response.StatusCode} {response.StatusCode} {url}");
Console.WriteLine($" -> {snippet}");
}
catch (Exception ex)
{
Console.WriteLine($"[{what,-22}] ERROR {url} -> {ex.Message}");
}
}
}
}
33 changes: 33 additions & 0 deletions samples/dotnet/minimal/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Minimal-API host sample (net10, Razor)

Demonstrates the opt-in **`UseAspNetCoreMinimal()`** path: a Hosuto web module is hosted on a
minimal-API `WebApplication` inner host instead of the classic `ConfigureWebHostDefaults` host. The
module (`RazorModule`) is an ordinary Hosuto `WebModule` using the module interfaces
(`IServiceConfiguringModule` / `IApplicationConfiguringModule`) and is authored exactly like a
module for the multi-host builder.

Run it:

```
dotnet run --project App
```

The host starts the module on an ephemeral port, then probes two URLs and prints the results.

## What works

- `GET /` → **200** — Razor Pages are discovered and rendered on the minimal-API module host.
- Dependency injection / SimpleInjector module container (`ConfigureContainer`) works post-build.

## Known limitation (tracked as a follow-up)

- `GET /css/site.css` → **404** — a module's **static web assets are not yet mapped** on the
minimal-API host.

The classic host wires module assets via `ModuleWebAssetsLoader`, which is **file-provider /
XML-manifest** based (`{app}.StaticWebAssets.xml`) and manipulates `WebRootFileProvider`. Since
.NET 9 static web assets are **endpoint-based** (`MapStaticAssets`,
`{app}.staticwebassets.*.json`), that mechanism no longer applies. Mapping a module's static web
assets on net9/10 (filtering the host manifest's `.modules/{module}` entries into the module host)
is a separate piece of work and is intentionally **not** part of the initial `UseAspNetCoreMinimal`
handler.
14 changes: 14 additions & 0 deletions samples/dotnet/minimal/RazorModule/Pages/Index.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@page
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<title>Razor module</title>
<link rel="stylesheet" href="/css/site.css" />
</head>
<body>
<h1 id="msg">Razor module page rendered</h1>
</body>
</html>
34 changes: 34 additions & 0 deletions samples/dotnet/minimal/RazorModule/RazorGreetModule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Dbosoft.Hosuto.Modules;

namespace Dbosoft.Hosuto.Samples.Minimal.RazorModule
{
// A Razor web module on the minimal-API WebApplication inner host, authored with the three
// module contracts - services, middleware, and (minimal-API-style) endpoints:
// IServiceConfiguringModule -> AddRazorPages
// IApplicationConfiguringModule -> UseStaticFiles (middleware)
// IEndpointConfiguringModule -> MapRazorPages (idiomatic endpoint mapping)
public sealed class RazorGreetModule
: WebModule, IServiceConfiguringModule, IApplicationConfiguringModule, IEndpointConfiguringModule
{
public override string Path { get; } = "";

public void ConfigureServices(IServiceProvider serviceProvider, IServiceCollection services)
{
services.AddRazorPages();
}

public void Configure(IServiceProvider serviceProvider, IApplicationBuilder app)
{
app.UseStaticFiles();
}

public void MapEndpoints(IServiceProvider serviceProvider, IEndpointRouteBuilder endpoints)
{
endpoints.MapRazorPages();
}
}
}
25 changes: 25 additions & 0 deletions samples/dotnet/minimal/RazorModule/RazorModule.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<OutputType>Library</OutputType>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<IsPackable>false</IsPackable>
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<RootNamespace>Dbosoft.Hosuto.Samples.Minimal.RazorModule</RootNamespace>
<AssemblyName>Dbosoft.Hosuto.Samples.Minimal.RazorModule</AssemblyName>
<!-- module static web assets are namespaced so the host can map them per module -->
<StaticWebAssetBasePath>.modules/$(AssemblyName)</StaticWebAssetBasePath>
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Hosuto.Hosting.AspNetCore\Hosuto.Hosting.AspNetCore.csproj" />
<ProjectReference Include="..\..\..\..\src\Hosuto.SimpleInjector\Hosuto.SimpleInjector.csproj" />
</ItemGroup>

</Project>
1 change: 1 addition & 0 deletions samples/dotnet/minimal/RazorModule/wwwroot/css/site.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#msg { color: green; }
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#if NET6_0_OR_GREATER
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;

namespace Dbosoft.Hosuto.Modules.Hosting
{
// Runs the module's Configure delegate when the request pipeline is built (at start), which is
// after the module container has been configured by the bootstrap pipeline.
internal sealed class ModuleConfigureStartupFilter : IStartupFilter
{
private readonly Action<IApplicationBuilder> _configure;

public ModuleConfigureStartupFilter(Action<IApplicationBuilder> configure)
{
_configure = configure;
}

public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return app =>
{
_configure(app);
next(app);
};
}
}

/// <summary>
/// Opt-in hook to configure the minimal-API <see cref="WebApplicationBuilder"/> of a web module
/// hosted via <c>UseAspNetCoreMinimal()</c>.
/// </summary>
public interface IWebApplicationBuilderConfigurer
{
void Configure(WebApplicationBuilder builder);
}

internal sealed class DelegateWebApplicationBuilderConfigurer : IWebApplicationBuilderConfigurer
{
private readonly Action<WebApplicationBuilder> _configure;

public DelegateWebApplicationBuilderConfigurer(Action<WebApplicationBuilder> configure)
{
_configure = configure;
}

public void Configure(WebApplicationBuilder builder) => _configure(builder);
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,29 @@ public static IModulesHostBuilder UseAspNetCoreWithDefaults(this IModulesHostBui
return UseAspNetCore(builder, configure);
}

#if NET6_0_OR_GREATER
/// <summary>
/// Opt-in: host web modules on a minimal-API <see cref="Microsoft.AspNetCore.Builder.WebApplication"/>
/// inner host instead of the classic <c>ConfigureWebHostDefaults</c> host. Additive - the
/// module authoring model (ConfigureServices/Configure/ConfigureContainer, convention or the
/// module interfaces) is unchanged.
/// </summary>
public static IModulesHostBuilder UseAspNetCoreMinimal(this IModulesHostBuilder builder,
Action<Microsoft.AspNetCore.Builder.WebApplicationBuilder> configure = null)
{
builder.ConfigureFrameworkServices((ctx, services) =>
{
services.AddTransient(typeof(IBootstrapHostFilter<>), typeof(WebApplicationBootstrapHostFilter<>));

if (configure != null)
services.AddTransient<IWebApplicationBuilderConfigurer>(sp =>
new DelegateWebApplicationBuilderConfigurer(configure));
});

return builder;
}
#endif

public static IModulesHostBuilder UseAspNetCore(this IModulesHostBuilder builder,
Action<IWebModule, Microsoft.AspNetCore.Hosting.IWebHostBuilder> configure = null)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#if NET6_0_OR_GREATER
using System;

namespace Dbosoft.Hosuto.Modules.Hosting
{
public class WebApplicationBootstrapHostFilter<TModule> : IBootstrapHostFilter<TModule> where TModule : class
{
public Action<BootstrapModuleHostCommand<TModule>> Invoke(Action<BootstrapModuleHostCommand<TModule>> next)
{
return command =>
{
var handler = new WebApplicationModuleHostHandler<TModule>();
handler.BootstrapHost(command);
next(command);
};
}
}
}
#endif
Loading