Hi ASP.NET Zero team,
I’m looking for some guidance around ASP.NET Zero’s hosting model and .NET Aspire compatibility.
I saw the official blog post on integrating .NET Aspire with ASP.NET Zero, and it was helpful as a starting point. However, that article appears to focus mainly on using Aspire as an orchestrator for SQL Server, the Migrator project, and the ASP.NET Zero web projects. It also explicitly says that a ServiceDefaults project is not used in the sample. From what I can tell, that means the sample does not really address the deeper Aspire integration points such as shared service defaults, OpenTelemetry, health checks, service discovery, resilience, and common configuration patterns.
In our project, we are using the ASP.NET Zero MVC / jQuery application, and our Program.cs still uses the older WebHostBuilder style:
public static void Main(string[] args)
{
AppContext.SetSwitch("Switch.Microsoft.Data.SqlClient.UseCompatibilityAsyncBehaviour", false);
AppContext.SetSwitch("Switch.Microsoft.Data.SqlClient.UseCompatibilityProcessSni", false);
CreateWebHostBuilder(args).Build().Run();
}
private static IWebHostBuilder CreateWebHostBuilder(string[] args)
{
return new WebHostBuilder()
.ConfigureAppConfiguration((hostingContext, config) =>
{
var env = hostingContext.HostingEnvironment;
config.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
if (env.IsDevelopment())
{
var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
config.AddUserSecrets(appAssembly, optional: true);
}
config.AddEnvironmentVariables();
if (args != null)
{
config.AddCommandLine(args);
}
})
.UseKestrel(opt =>
{
opt.AddServerHeader = false;
opt.Limits.MaxRequestLineSize = 16 * 1024;
opt.Limits.MaxRequestHeadersTotalSize = 1048576;
opt.ConfigureEndpointDefaults(listenOptions => listenOptions.UseConnectionLogging());
})
.UseContentRoot(Directory.GetCurrentDirectory())
.ConfigureLogging((_, logging) =>
{
logging.AddFilter("Microsoft.AspNetCore", LogLevel.Warning);
logging.AddFilter("Microsoft.EntityFrameworkCore.Database.Command", LogLevel.Warning);
logging.AddFilter("Microsoft.Extensions.Caching.Memory", LogLevel.None);
})
.UseIIS()
.UseIISIntegration()
.UseStartup<Startup>();
}
At one point I attempted to change this to be closer to the modern Host.CreateDefaultBuilder(...) / minimal-hosting style so that we could use an Aspire ServiceDefaults project, but I was not able to get it working cleanly with the existing ASP.NET Zero startup/module structure.
My main questions are:
Is there an officially recommended way to migrate an ASP.NET Zero MVC application from the current
WebHostBuildermodel to a hosting model that is more compatible with .NET Aspire’sServiceDefaultspattern?Are there known ASP.NET Zero-specific concerns when changing from
new WebHostBuilder().UseStartup<Startup>()toHost.CreateDefaultBuilder(args).ConfigureWebHostDefaults(...)orWebApplication.CreateBuilder(args)?Is the ASP.NET Zero team planning to update the project template to use a more Aspire-compatible hosting model?
If the current recommendation is to avoid
ServiceDefaults, is the intended approach to manually copy the relevant pieces intoStartup.ConfigureServices, such as OpenTelemetry, health checks, service discovery, and resilience?Are there any examples of ASP.NET Zero using Aspire for more than orchestration — specifically OpenTelemetry/exporters, service defaults, or integration with local observability tools?
Our current goal is to run the ASP.NET Zero application under Aspire locally, alongside dependencies such as SQL Server, Redis, and an observability stack like OpenObserve. We can run the application from Aspire, but the bigger question is whether there is a supported path to align the ASP.NET Zero hosting model with the way Aspire expects modern ASP.NET Core applications to be configured.
Any guidance or recommended migration path would be appreciated.
1 Answer(s)
-
0
Hi @kfrancis
Thanks for the detailed and well researched question, you've correctly identified the exact gap. Let me address it point by point, but first there's one framing correction that changes most of the answer.
The key point first: you're comparing against an older template
The
Program.csyou pasted(new WebHostBuilder().UseStartup<Startup>())is from an older ASP.NET Zero version. The current ASP.NET Zero MVC/jQuery template no longer usesWebHostBuilderat all, it already runs on the .NET Generic Host:public static IHostBuilder CreateHostBuilder(string[] args) { return Host.CreateDefaultBuilder(args) .UseCastleWindsor(IocManager.Instance.IocContainer) // ABP DI container as the provider factory .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseKestrel(opt => { opt.AddServerHeader = false; opt.Limits.MaxRequestLineSize = 16 * 1024; }); webBuilder.UseContentRoot(Directory.GetCurrentDirectory()); webBuilder.UseIIS(); webBuilder.UseStartup<Startup>(); }) .ConfigureLogging((context, logging) => { logging.AddFilter("Microsoft.EntityFrameworkCore.Database.Command", LogLevel.Warning); }); }So the hosting model modernization you're trying to do is largely already done in the current template. The single most important, low risk step is to rebase your
Program.csonto this shape. There are two things to note:On .NET 10, new
WebHostBuilder()is now obsolete(compiler warning ASPDEPR004; IWebHost/WebHost emit ASPDEPR008). Moving to the Generic Host clears these.The Generic Host is not deprecated and is supported indefinitely, you do not need
WebApplication.CreateBuilder/ minimal hosting to be "modern" or to use Aspire (more on this below). This is very likely why your earlier attempt didn't come together cleanly: you were reaching for minimal hosting when the Generic Host is the right target.1) Officially recommended migration path
Target the Generic Host shape shown above
(Host.CreateDefaultBuilder(args).UseCastleWindsor(...).ConfigureWebHostDefaults(w => w.UseStartup<Startup>())), keeping your existing Startup class. This preserves Castle Windsor, Log4Net, ABP module wiring, and your entire pipeline unchanged.We recommend against going all the way to
WebApplication.CreateBuilder(args)(minimal hosting), it's a larger, riskier change with no upside for Aspire.2) ASP.NET Zero specific concerns when changing the hosting model
This is the part that trips most people up, and it's ABP specific:
Moving to the Generic Host (recommended):
The Castle Windsor line is load bearing.
.UseCastleWindsor(IocManager.Instance.IocContainer)must sit on theIHostBuilder, this registers Castle Windsor as the host'sIServiceProviderFactory. If you drop it, ABP DI resolution breaks at startup.Keep
services.AddAbpWithoutCreatingServiceProvider<AbpZeroTemplateWebMvcModule>()inStartup.ConfigureServices(not the older AddAbp<T>(), which returns an IServiceProvider, that's the pre 3.0 pattern). The"WithoutCreatingServiceProvider"variant only populates theIServiceCollectionand lets the host build the provider via the Windsor factory. This is exactly what the Generic Host expects.app.UseAbp(...)stays where it is in the pipeline.If you were to go to
WebApplication.CreateBuilderanyway (not recommended):WebApplicationBuilderdoes not supportUseStartup, bothbuilder.WebHost.UseStartup<Startup>()andbuilder.Host.ConfigureWebHostDefaults(w => w.UseStartup<Startup>())throw at runtime, and analyzer ASP0010 flags it at build (Microsoft says don't suppress it).Castle Windsor does survive minimal hosting, builder.Host is an IHostBuilder, so
builder.Host.UseCastleWindsor(IocManager.Instance.IocContainer)(orbuilder.Host.UseServiceProviderFactory(new WindsorServiceProviderFactory())) is valid.You can even keep the Startup class and invoke it manually
(new Startup(...).ConfigureServices(builder.Services)…startup.Configure(app, app.Environment)).But you inherit real gotchas: host setting mutations like
WebHost.UseContentRoot/UseEnvironment/UseSetting(...)now throw (use WebApplicationOptions/ASPNETCORE_* instead); the app name defaults to the entry assembly (can affect MVC application part discovery); the dev exception page is on by default; and there's no automatic DI scope at startup.Honest caveat: legacy ABP on full minimal hosting is technically feasible (it's the standard custom DI container pattern), but it is not an officially templated or tested ABP scenario. There's no ABP Castle sample for it. We wouldn't recommend it just to enable Aspire, because you don't need it.
3) Is the template moving to a more Aspire compatible hosting model?
Two separate things here:
Hosting model: already done. The current template is on the Generic Host
(Host.CreateDefaultBuilder + UseStartup<Startup>)with a config gated /health endpoint(AddAbpZeroHealthCheck() / UseHealthChecks("/health")).First class
ServiceDefaults(OpenTelemetry/service discovery/resilience) baked into the template: this is not shipped today and was deliberately left out of the official Aspire sample to keep it simple.The building blocks all work on the current template, so today this is a supported but manual customer task rather than a turnkey template feature. (I'm not quoting a delivery date here, treat ServiceDefaults integration as "manual recipe available now.")
4) Yes, manually applying the
ServiceDefaultspieces intoStartup.ConfigureServicesis the intended approachThis is the crux, and the good news: you do not need a
ServiceDefaultsproject orAddServiceDefaults()at all.AddServiceDefaults()is just a convenience extension constrained toIHostApplicationBuilder(which onlyWebApplicationBuilder/HostApplicationBuilderimplement, the Generic Host's IHostBuilder does not, which is why you can't call it directly). But everything inside it bottoms out at IServiceCollection / ILoggingBuilder extensions, and you already hold both in your Startup/Program. So you replace builder.Services with services and wire the exact same building blocks.Packages to add:
- OpenTelemetry.Extensions.Hosting,
- OpenTelemetry.Instrumentation.AspNetCore,
- OpenTelemetry.Instrumentation.Http,
- OpenTelemetry.Instrumentation.Runtime,
- OpenTelemetry.Exporter.OpenTelemetryProtocol,
- Microsoft.Extensions.ServiceDiscovery,
- Microsoft.Extensions.Http.Resilience.
In
Startup.ConfigureServices(IServiceCollection services):services.AddOpenTelemetry() .WithMetrics(m => m.AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() .AddRuntimeInstrumentation()) .WithTracing(t => t.AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation()); // OTLP exporter, gated exactly like ServiceDefaults does it: if (!string.IsNullOrWhiteSpace(_appConfiguration["OTEL_EXPORTER_OTLP_ENDPOINT"])) services.AddOpenTelemetry().UseOtlpExporter(); // call once; do NOT mix with per signal AddOtlpExporter services.AddServiceDiscovery(); services.ConfigureHttpClientDefaults(http => { http.AddStandardResilienceHandler(); http.AddServiceDiscovery(); }); // KEEP the existing ASP.NET Zero health check, this is what Aspire polls: services.AddAbpZeroHealthCheck(); // optional Aspire style liveness probe: services.AddHealthChecks().AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);In
Program.ConfigureLogging(the one piece that targets ILoggingBuilder, not IServiceCollection):.ConfigureLogging((ctx, logging) => { logging.AddOpenTelemetry(o => { o.IncludeFormattedMessage = true; o.IncludeScopes = true; }); });Leave your
Startup.Configureas is, including the existingapp.UseHealthChecks("/health", ...). Don't double-map /health, you already have it. Enable it with"HealthChecksEnabled": trueinappsettings.json(health checks are off by default in AZ). This is exactly what the official Aspire sample relies on via WithHttpHealthCheck("/health").One important ABP caveat on logs: ASP.NET Zero/ABP writes logs through Castle's
Castle.Core.Logging.ILogger(backed by Log4Net), which is a different abstraction fromMicrosoft.Extensions.Logging.ILogger. logging.AddOpenTelemetry()only captures the Microsoft ILogger pipeline, so application code logging via the injected ABP/Castle ILogger will not appear in OTel logs (traces and metrics are unaffected). To close that gap you either (a) add a Log4Net appender that forwards events through an ILoggerFactory wired to OTLP, or (b) ingest the Log4Net rolling file output into OpenObserve out of band.5) Aspire beyond orchestration + your SQL Server / Redis / OpenObserve goal
The official sample (Integrate .NET Aspire with ASP.NET Zero) is intentionally orchestration only (SQL Server in Docker → Migrator → the three web UIs, sequenced with WaitFor; it explicitly says "we will not use [a service defaults project] in this sample to keep it simple"). There is no official end to end OpenTelemetry/OpenObserve sample yet. But combining with a few AppHost lines gets you there:
OTLP export is driven by standard env vars (read via IConfiguration, so appsettings.json works too): OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_PROTOCOL (http/protobuf→:4318, grpc→:4317), OTEL_EXPORTER_OTLP_HEADERS.
OpenObserve (self hosted) accepts OTLP/HTTP at http://localhost:5080/api/<org> and requires an Authorization: Basic <base64(email:password)> header (plus a stream name header):
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:5080/api/default OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf OTEL_EXPORTER_OTLP_HEADERS=Authorization=Basic <base64>,stream name=default(If you route through an OTel Collector's otlphttp/openobserve exporter instead, the endpoint must have no trailing slash, the collector appends /v1/<signal>.)
Aspire AppHost can orchestrate the backing services and point the apps at OpenObserve:
var sql = builder.AddSqlServer("Sql").WithLifetime(ContainerLifetime.Persistent); var db = sql.AddDatabase("ZeroDb"); var redis = builder.AddRedis("Redis"); var oo = builder.AddContainer("openobserve", "public.ecr.aws/zinclabs/openobserve"); var migrator = builder.AddProject<Projects.Migrator>("Migrator") .WithEnvironment("ASPNETCORE_Docker_Enabled", "true") // runs non interactively, then exits .WithReference(db, connectionName: "Default") // maps to ConnectionStrings:Default .WaitFor(db); builder.AddProject<Projects.Web_Mvc>("Admin UI", launchProfileName: "…") .WithReference(db, connectionName: "Default") .WaitFor(migrator) .WithHttpHealthCheck("/health") .WithEnvironment("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:5080/api/default") .WithEnvironment("OTEL_EXPORTER_OTLP_HEADERS", "Authorization=Basic <base64>,stream name=default");Two Aspire caveats to be aware of:
As of Aspire 9.4, when your app runs under the AppHost, Aspire overwrites OTEL_EXPORTER_OTLP_ENDPOINT with its own dashboard endpoint at launch. To reach OpenObserve you set the values at the resource level (as above) and/or run a Collector that fans out to both the Aspire dashboard and OpenObserve. (If you run the AZ app standalone, not under the AppHost, nothing overrides your env vars and export to OpenObserve is straightforward.)
WithOtlpExporter() is incomplete for container resources, inject OTEL vars via WithEnvironment(...) for those.
Markdown is supportedCopy & paste or drag & drop images (max 30 MB per image)