Skip to content
Open
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
7 changes: 7 additions & 0 deletions Aspirant.sln
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspirant.Hosting.PostgreSQL
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspirant.Hosting.RabbitMQ", "src\Aspirant.Hosting.RabbitMQ\Aspirant.Hosting.RabbitMQ.csproj", "{45EC9DE1-B3E7-4EF4-A0AA-8CD7230EB7FB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspirant.Hosting.WebApplication", "src\Aspirant.Hosting.WebApplication\Aspirant.Hosting.WebApplication.csproj", "{BA7B7E8C-EBC1-495E-86FB-56DA477AA4F4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -110,6 +112,10 @@ Global
{45EC9DE1-B3E7-4EF4-A0AA-8CD7230EB7FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{45EC9DE1-B3E7-4EF4-A0AA-8CD7230EB7FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{45EC9DE1-B3E7-4EF4-A0AA-8CD7230EB7FB}.Release|Any CPU.Build.0 = Release|Any CPU
{BA7B7E8C-EBC1-495E-86FB-56DA477AA4F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BA7B7E8C-EBC1-495E-86FB-56DA477AA4F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BA7B7E8C-EBC1-495E-86FB-56DA477AA4F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BA7B7E8C-EBC1-495E-86FB-56DA477AA4F4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -130,6 +136,7 @@ Global
{135004A9-F7E0-46D9-B3D0-3D64BA3C40B9} = {6E773CFD-9758-4C09-96F6-5EDCED0E3A34}
{77D17495-C537-4446-8C2E-8B46560E0992} = {6E773CFD-9758-4C09-96F6-5EDCED0E3A34}
{45EC9DE1-B3E7-4EF4-A0AA-8CD7230EB7FB} = {6E773CFD-9758-4C09-96F6-5EDCED0E3A34}
{BA7B7E8C-EBC1-495E-86FB-56DA477AA4F4} = {6E773CFD-9758-4C09-96F6-5EDCED0E3A34}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {52627286-38AF-4E95-BDA0-B21166B2B18C}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Description>A custom ASP.NET Core web app resource for .NET Aspire App Host projects.</Description>
<PackageTags>aspire hosting aspnet aspnetcore</PackageTags>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting" />
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Aspirant.Hosting\Aspirant.Hosting.csproj" />
</ItemGroup>

</Project>
225 changes: 225 additions & 0 deletions src/Aspirant.Hosting.WebApplication/WebApplicationResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
using Aspire.Hosting;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Lifecycle;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Aspirant.Hosting;

/// <summary>
/// Represents a ASP.NET Web Application resource.
/// </summary>
public class WebApplicationResource(string name) : Resource(name), IResourceWithServiceDiscovery, IResourceWithEnvironment
{

}

public abstract class WebApplicationResourceLifecycleHook<TResource>(
IHostEnvironment hostEnvironment,
DistributedApplicationExecutionContext executionContext,
ResourceNotificationService resourceNotificationService,
ResourceLoggerService resourceLoggerService) : IDistributedApplicationLifecycleHook, IAsyncDisposable
where TResource : WebApplicationResource
{
protected abstract string ResourceTypeName { get; }

private WebApplication? _app;

/// <inheritdoc />
public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
{
if (executionContext.IsPublishMode)
{
return;
}

var resource = appModel.Resources.OfType<TResource>().SingleOrDefault();

if (resource is null)
{
return;
}

await resourceNotificationService.PublishUpdateAsync(resource, s => s with
{
ResourceType = ResourceTypeName,
State = "Starting"
});

var bindings = resource.Annotations.OfType<EndpointAnnotation>().ToList();

foreach (var b in bindings)
{
b.IsProxied = false;
}
}

/// <inheritdoc />
public async Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default)
{
if (executionContext.IsPublishMode)
{
return;
}

var resource = appModel.Resources.OfType<TResource>().SingleOrDefault();

if (resource is null)
{
return;
}

var builder = WebApplication.CreateSlimBuilder(new WebApplicationOptions
{
ContentRootPath = hostEnvironment.ContentRootPath,
EnvironmentName = hostEnvironment.EnvironmentName,
WebRootPath = Path.Combine(hostEnvironment.ContentRootPath, "wwwroot")
});

builder.Logging.ClearProviders();

builder.Logging.AddProvider(new ResourceLoggerProvider(resourceLoggerService.GetLogger(resource.Name)));

if (resource.TryGetEnvironmentVariables(out var environmentVariables))
{
var context = new EnvironmentCallbackContext(executionContext, cancellationToken: cancellationToken);

foreach (var cb in environmentVariables)
{
await cb.Callback(context);
}

var dict = new Dictionary<string, string?>();
foreach (var (k, v) in context.EnvironmentVariables)
{
var val = v switch
{
string s => s,
IValueProvider vp => await vp.GetValueAsync(context.CancellationToken),
_ => throw new NotSupportedException()
};

if (val is not null)
{
dict[k.Replace("__", ":")] = val;
}
}

builder.Configuration.AddInMemoryCollection(dict);
}

await ConfigureBuilderAsync(builder, resource, cancellationToken);

resource.TryGetEndpoints(out var endpoints);
var defaultScheme = Environment.GetEnvironmentVariable("ASPNETCORE_URLS")?.Contains("https://") == true ? "https" : "http";
var needHttps = defaultScheme == "https" || endpoints?.Any(ep => ep.UriScheme == "https") == true;

if (needHttps)
{
builder.WebHost.UseKestrelHttpsConfiguration();
}

_app = builder.Build();

var urlToEndpointNameMap = new Dictionary<string, string>();

if (endpoints is null)
{
var url = $"{defaultScheme}://127.0.0.1:0/";
_app.Urls.Add(url);
urlToEndpointNameMap[url] = "default";
}
else
{
foreach (var ep in endpoints)
{
var scheme = ep.UriScheme ?? defaultScheme;
needHttps = needHttps || scheme == "https";

var url = ep.Port switch
{
null => $"{scheme}://127.0.0.1:0/",
_ => $"{scheme}://localhost:{ep.Port}"
};

var uri = new Uri(url);
_app.Urls.Add(url);
urlToEndpointNameMap[uri.ToString()] = ep.Name;
}
}

await ConfigureApplicationAsync(_app, resource, cancellationToken);

await _app.StartAsync(cancellationToken);

var addresses = _app.Services.GetRequiredService<IServer>().Features.GetRequiredFeature<IServerAddressesFeature>().Addresses;

foreach (var url in addresses)
{
if (urlToEndpointNameMap.TryGetValue(new Uri(url).ToString(), out var name)
|| urlToEndpointNameMap.TryGetValue((new UriBuilder(url) { Port = 0 }).Uri.ToString(), out name))
{
var ep = endpoints?.FirstOrDefault(ep => ep.Name == name);
if (ep is not null)
{
var uri = new Uri(url);
var host = uri.Host is "127.0.0.1" or "[::1]" ? "localhost" : uri.Host;
ep.AllocatedEndpoint = new(ep, host, uri.Port);
}
}
}

await resourceNotificationService.PublishUpdateAsync(resource, s => s with
{
State = "Running",
Urls = [.. endpoints?.Select(ep => new UrlSnapshot(ep.Name, ep.AllocatedEndpoint?.UriString ?? "", IsInternal: false))],
});
}

protected abstract ValueTask ConfigureBuilderAsync(WebApplicationBuilder builder, TResource resource, CancellationToken cancellationToken);

protected abstract ValueTask ConfigureApplicationAsync(WebApplication application, TResource resource, CancellationToken cancellationToken);

/// <inheritdoc />
public ValueTask DisposeAsync()
{
return _app?.DisposeAsync() ?? default;
}

private class ResourceLoggerProvider(ILogger logger) : ILoggerProvider
{
public ILogger CreateLogger(string categoryName)
{
return new ResourceLogger(logger);
}

public void Dispose()
{
}

private class ResourceLogger(ILogger logger) : ILogger
{
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
{
return logger.BeginScope(state);
}

public bool IsEnabled(LogLevel logLevel)
{
return logger.IsEnabled(logLevel);
}

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
logger.Log(logLevel, eventId, state, exception, formatter);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Aspire.Hosting;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Lifecycle;

namespace Aspirant.Hosting;


/// <summary>
/// Extensions method to add WebApplication resources.
/// </summary>
public static class WebApplicationResourceExtensions
{
public static IResourceBuilder<TResource> AddWebApplication<TResource, TLifecycleHook>(this IDistributedApplicationBuilder builder, TResource resource, bool excludeFromManifest = false)
where TResource : WebApplicationResource
where TLifecycleHook : WebApplicationResourceLifecycleHook<TResource>
{
var webApplicationResource = builder.Resources.OfType<TResource>().SingleOrDefault();

if (webApplicationResource is not null)
{
throw new InvalidOperationException($"A resource of type {nameof(TResource)} has already been added to this application");
}

builder.Services.TryAddLifecycleHook<TLifecycleHook>();

var resourceBuilder = builder.AddResource(resource);

if (excludeFromManifest)
{
resourceBuilder = resourceBuilder.ExcludeFromManifest();
}

return resourceBuilder;
}
}
1 change: 1 addition & 0 deletions src/Aspirant.Hosting.Yarp/Aspirant.Hosting.Yarp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

<ItemGroup>
<ProjectReference Include="..\Aspirant.Hosting\Aspirant.Hosting.csproj" />
<ProjectReference Include="..\Aspirant.Hosting.WebApplication\Aspirant.Hosting.WebApplication.csproj" />
</ItemGroup>

</Project>
Loading