Skip to content

Commit

Permalink
mapping endpoints on worker for ASP.NET proxy integration (#1596)
Browse files Browse the repository at this point in the history
  • Loading branch information
brettsam authored and fabiocav committed Jun 19, 2023
1 parent c309918 commit 69398e4
Show file tree
Hide file tree
Showing 7 changed files with 348 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

namespace Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore
{
/// <summary>
/// Represents an HttpTrigger binding. Internal class for deserializing raw binding info.
/// </summary>
internal class FunctionHttpBinding
{
public string Name { get; set; } = default!;

public string Type { get; set; } = default!;

public string Route { get; set; } = default!;

public string[] Methods { get; set; } = default!;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Patterns;
using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;

namespace Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore.AspNetMiddleware
{
internal class FunctionsEndpointDataSource : EndpointDataSource
{
private const string FunctionsApplicationDirectoryKey = "FUNCTIONS_APPLICATION_DIRECTORY";
private const string HostJsonFileName = "host.json";
private const string DefaultRoutePrefix = "api";

private readonly IFunctionMetadataProvider _functionMetadataProvider;
private readonly object _lock = new();

private static readonly JsonSerializerOptions _jsonSerializerOptions = new()
{
PropertyNameCaseInsensitive = true
};


private List<Endpoint>? _endpoints;

public FunctionsEndpointDataSource(IFunctionMetadataProvider functionMetadataProvider)
{
_functionMetadataProvider = functionMetadataProvider ?? throw new ArgumentNullException(nameof(functionMetadataProvider));
}

public override IReadOnlyList<Endpoint> Endpoints
{
get
{
if (_endpoints is null)
{
lock (_lock)
{
_endpoints ??= BuildEndpoints();
}
}

return _endpoints;
}
}

private List<Endpoint> BuildEndpoints()
{
List<Endpoint> endpoints = new List<Endpoint>();

string scriptRoot = Environment.GetEnvironmentVariable(FunctionsApplicationDirectoryKey) ??
throw new InvalidOperationException("Cannot determine script root directory.");

var metadata = _functionMetadataProvider.GetFunctionMetadataAsync(scriptRoot).GetAwaiter().GetResult();

string routePrefix = GetRoutePrefixFromHostJson(scriptRoot) ?? DefaultRoutePrefix;

foreach (var functionMetadata in metadata)
{
var endpoint = MapHttpFunction(functionMetadata, routePrefix);

if (endpoint is not null)
{
endpoints.Add(endpoint);
}
}

return endpoints;
}

public override IChangeToken GetChangeToken() => NullChangeToken.Singleton;

internal static Endpoint? MapHttpFunction(IFunctionMetadata functionMetadata, string routePrefix)
{
if (functionMetadata.RawBindings is null)
{
return null;
}

var functionName = functionMetadata.Name ?? string.Empty;

int order = 0;
foreach (var binding in functionMetadata.RawBindings)
{
var functionBinding = JsonSerializer.Deserialize<FunctionHttpBinding>(binding, _jsonSerializerOptions);

if (functionBinding is null)
{
continue;
}

if (functionBinding.Type.Equals("httpTrigger", StringComparison.OrdinalIgnoreCase))
{
string routeSuffix = functionBinding.Route ?? functionName;
string route = $"{routePrefix}/{routeSuffix}";

var pattern = RoutePatternFactory.Parse(route);

var endpointBuilder = new RouteEndpointBuilder(FunctionsHttpContextExtensions.InvokeFunctionAsync, pattern, order++)
{
DisplayName = functionName
};
endpointBuilder.Metadata.Add(new HttpMethodMetadata(functionBinding.Methods));

// no need to look at other bindings for this function
return endpointBuilder.Build();
}
}

return null;
}

private static string? GetRoutePrefixFromHostJson(string scriptRoot)
{
string hostJsonPath = Path.Combine(scriptRoot, HostJsonFileName);

if (!File.Exists(hostJsonPath))
{
return null;
}

string hostJsonString = File.ReadAllText(hostJsonPath);
return GetRoutePrefix(hostJsonString);
}

internal static string? GetRoutePrefix(string hostJsonString)
{
var hostJson = JsonSerializer.Deserialize<HostJsonModel>(hostJsonString, _jsonSerializerOptions);
return hostJson?.Extensions?.Http?.RoutePrefix;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System.Threading.Tasks;
using Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNetCore.Http
{
internal static class FunctionsHttpContextExtensions
{
internal static Task InvokeFunctionAsync(this HttpContext context)
{
var coordinator = context.RequestServices.GetRequiredService<IHttpCoordinator>();
context.Request.Headers.TryGetValue(Constants.CorrelationHeader, out StringValues invocationId);
return coordinator.RunFunctionInvocationAsync(invocationId);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore
{
/// <summary>
/// Represents host.json. Internal class for deserializing.
/// </summary>
internal class HostJsonModel
{
public HostJsonExtensionModel Extensions { get; set; } = default!;
}

internal class HostJsonExtensionModel
{
public HostJsonExtensionHttpModel Http { get; set; } = default!;
}

internal class HostJsonExtensionHttpModel
{
public string RoutePrefix { get; set; } = default!;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,18 @@

using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;

namespace Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore
{
internal class InvokeFunctionMiddleware
{
private readonly IHttpCoordinator _coordinator;

public InvokeFunctionMiddleware(RequestDelegate next, IHttpCoordinator httpCoordinator)
public InvokeFunctionMiddleware(RequestDelegate next)
{
_coordinator = httpCoordinator;
}

public Task Invoke(HttpContext context)
{
context.Request.Headers.TryGetValue(Constants.CorrelationHeader, out StringValues invocationId);
return _coordinator.RunFunctionInvocationAsync(invocationId);
return context.InvokeFunctionAsync();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore;
using Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore.AspNetMiddleware;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.Extensions.Hosting
{
/// <summary>
/// Provides extension methods to work with a <see cref="IHostBuilder"/>.
/// </summary>
public static class HostBuilderExtensions
public static class FunctionsHostBuilderExtensions
{
/// <summary>
/// Configures the worker to use the ASP.NET Core integration, enabling advanced HTTP features.
Expand Down Expand Up @@ -46,14 +47,24 @@ public static IHostBuilder ConfigureFunctionsWebApplication(this IHostBuilder bu

internal static IHostBuilder ConfigureAspNetCoreIntegration(this IHostBuilder builder)
{
builder.ConfigureServices(services =>
{
services.AddSingleton<FunctionsEndpointDataSource>();
});

builder.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseUrls(HttpUriProvider.HttpUriString);
webBuilder.Configure(b =>
{
b.UseRouting();
b.UseMiddleware<WorkerRequestServicesMiddleware>();
// TODO: provide a way for customers to configure their middleware pipeline here
b.UseMiddleware<InvokeFunctionMiddleware>();
// TODO: provide a way for customers to configure their middleware pipeline here
b.UseEndpoints(endpoints =>
{
var dataSource = endpoints.ServiceProvider.GetRequiredService<FunctionsEndpointDataSource>();
endpoints.DataSources.Add(dataSource);
});
});
});

Expand Down
Loading

0 comments on commit 69398e4

Please sign in to comment.