Skip to content

Commit

Permalink
Extracted parts of Yarp resource into abstract WebApplication resource
Browse files Browse the repository at this point in the history
  • Loading branch information
Kralizek committed Jun 10, 2024
1 parent 245520b commit b1bce36
Show file tree
Hide file tree
Showing 7 changed files with 598 additions and 240 deletions.
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

0 comments on commit b1bce36

Please sign in to comment.