Skip to content

Commit 06e6302

Browse files
Implementing function metadata transform support (#3145)
* Implementing function metadata transform support * Updates to metadata transform feature --------- Co-authored-by: Fabio Cavalcante <[email protected]>
1 parent f2f4165 commit 06e6302

File tree

8 files changed

+217
-11
lines changed

8 files changed

+217
-11
lines changed

release_notes.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010

1111
### Microsoft.Azure.Functions.Worker.Core <version>
1212

13-
- <entry>
13+
- Support for function metadata transforms (#3145)
1414

1515
### Microsoft.Azure.Functions.Worker.Grpc <version>
1616

17-
- <entry>
17+
- Updated to use the new metadata manage and leverage metadata transforms (#3145)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Collections.Immutable;
7+
using System.Linq;
8+
using System.Threading.Tasks;
9+
using Microsoft.Extensions.Logging;
10+
11+
namespace Microsoft.Azure.Functions.Worker.Core.FunctionMetadata
12+
{
13+
internal sealed class DefaultFunctionMetadataManager : IFunctionMetadataManager
14+
{
15+
private readonly IFunctionMetadataProvider _functionMetadataProvider;
16+
private readonly ImmutableArray<IFunctionMetadataTransformer> _transformers;
17+
private readonly ILogger<DefaultFunctionMetadataManager> _logger;
18+
19+
public DefaultFunctionMetadataManager(IFunctionMetadataProvider functionMetadataProvider,
20+
IEnumerable<IFunctionMetadataTransformer> transformers,
21+
ILogger<DefaultFunctionMetadataManager> logger)
22+
{
23+
_functionMetadataProvider = functionMetadataProvider;
24+
_transformers = transformers.ToImmutableArray();
25+
_logger = logger;
26+
}
27+
28+
public async Task<ImmutableArray<IFunctionMetadata>> GetFunctionMetadataAsync(string directory)
29+
{
30+
ImmutableArray<IFunctionMetadata> functionMetadata = await _functionMetadataProvider.GetFunctionMetadataAsync(directory);
31+
32+
return ApplyTransforms(functionMetadata);
33+
}
34+
35+
private ImmutableArray<IFunctionMetadata> ApplyTransforms(ImmutableArray<IFunctionMetadata> functionMetadata)
36+
{
37+
// Return early if there are no transformers to apply
38+
if (_transformers.Length == 0)
39+
{
40+
return functionMetadata;
41+
}
42+
43+
var metadataResult = functionMetadata.ToBuilder();
44+
45+
foreach (var transformer in _transformers)
46+
{
47+
try
48+
{
49+
_logger?.LogTrace("Applying metadata transformer: {Transformer}.", transformer.Name);
50+
transformer.Transform(metadataResult);
51+
}
52+
catch (Exception exc)
53+
{
54+
_logger?.LogError(exc, "Metadata transformer '{Transformer}' failed.", transformer.Name);
55+
throw;
56+
}
57+
}
58+
59+
return metadataResult.ToImmutable();
60+
}
61+
}
62+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System.Collections.Immutable;
5+
using System.Threading.Tasks;
6+
7+
namespace Microsoft.Azure.Functions.Worker.Core.FunctionMetadata
8+
{
9+
/// <summary>
10+
/// Manages function metadata, providing functionality that combines metadata from the registered provider and metadata transforms.
11+
/// </summary>
12+
public interface IFunctionMetadataManager
13+
{
14+
/// <summary>
15+
/// Retrieves all function metadata for the current application.
16+
/// </summary>
17+
/// <returns>A <see cref="Task{TResult}"/> representing the asynchronous metadata retrieval operation, where the result is an <see cref="ImmutableArray{IFunctionMetadata}"/>.</returns>
18+
Task<ImmutableArray<IFunctionMetadata>> GetFunctionMetadataAsync(string directory);
19+
}
20+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
6+
namespace Microsoft.Azure.Functions.Worker.Core.FunctionMetadata;
7+
8+
/// <summary>
9+
/// Defines a contract for transforming <see cref="IFunctionMetadata"/> instances for Azure Functions.
10+
/// Implementations can modify, augment, or filter function metadata before it is used by the host.
11+
/// </summary>
12+
public interface IFunctionMetadataTransformer
13+
{
14+
/// <summary>
15+
/// Gets the name of the transformer.
16+
/// </summary>
17+
public string Name { get; }
18+
19+
/// <summary>
20+
/// Transforms the provided collection of <see cref="IFunctionMetadata"/> instances.
21+
/// </summary>
22+
/// <param name="original">The original collection of function metadata. This collection may be modified.</param>
23+
void Transform(IList<IFunctionMetadata> original);
24+
}

src/DotNetWorker.Core/Hosting/ServiceCollectionExtensions.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@
1010
using Microsoft.Azure.Functions.Worker.Context.Features;
1111
using Microsoft.Azure.Functions.Worker.Converters;
1212
using Microsoft.Azure.Functions.Worker.Core;
13+
using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata;
1314
using Microsoft.Azure.Functions.Worker.Diagnostics;
1415
using Microsoft.Azure.Functions.Worker.Invocation;
1516
using Microsoft.Azure.Functions.Worker.Logging;
1617
using Microsoft.Azure.Functions.Worker.OutputBindings;
1718
using Microsoft.Azure.Functions.Worker.Pipeline;
1819
using Microsoft.Extensions.DependencyInjection.Extensions;
19-
using Microsoft.Extensions.Hosting;
2020
using Microsoft.Extensions.Logging;
2121
using Microsoft.Extensions.Options;
2222

@@ -71,6 +71,9 @@ public static IFunctionsWorkerApplicationBuilder AddFunctionsWorkerCore(this ISe
7171
// Worker initialization service
7272
services.AddHostedService<WorkerHostedService>();
7373

74+
// Worker metadata management
75+
services.TryAddSingleton<IFunctionMetadataManager, DefaultFunctionMetadataManager>();
76+
7477
// Default serializer settings
7578
services.AddOptions();
7679
services.TryAddEnumerable(ServiceDescriptor.Transient<IPostConfigureOptions<WorkerOptions>, WorkerOptionsSetup>());

src/DotNetWorker.Grpc/GrpcWorker.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7-
using System.Runtime.CompilerServices;
87
using System.Runtime.InteropServices;
98
using System.Text.RegularExpressions;
109
using System.Threading;
@@ -18,7 +17,6 @@
1817
using Microsoft.Azure.Functions.Worker.Invocation;
1918
using Microsoft.Azure.Functions.Worker.Rpc;
2019
using Microsoft.Extensions.Hosting;
21-
using Microsoft.Extensions.Logging;
2220
using Microsoft.Extensions.Options;
2321
using MsgType = Microsoft.Azure.Functions.Worker.Grpc.Messages.StreamingMessage.ContentOneofCase;
2422

@@ -32,14 +30,14 @@ internal partial class GrpcWorker : IWorker, IMessageProcessor
3230
private readonly IHostApplicationLifetime _hostApplicationLifetime;
3331
private readonly IWorkerClientFactory _workerClientFactory;
3432
private readonly IInvocationHandler _invocationHandler;
35-
private readonly IFunctionMetadataProvider _functionMetadataProvider;
33+
private readonly IFunctionMetadataManager _metadataManager;
3634
private IWorkerClient? _workerClient;
3735

3836
public GrpcWorker(IFunctionsApplication application,
3937
IWorkerClientFactory workerClientFactory,
4038
IMethodInfoLocator methodInfoLocator,
4139
IOptions<WorkerOptions> workerOptions,
42-
IFunctionMetadataProvider functionMetadataProvider,
40+
IFunctionMetadataManager metadataManager,
4341
IHostApplicationLifetime hostApplicationLifetime,
4442
IInvocationHandler invocationHandler)
4543
{
@@ -48,7 +46,7 @@ public GrpcWorker(IFunctionsApplication application,
4846
_application = application ?? throw new ArgumentNullException(nameof(application));
4947
_methodInfoLocator = methodInfoLocator ?? throw new ArgumentNullException(nameof(methodInfoLocator));
5048
_workerOptions = workerOptions.Value ?? throw new ArgumentNullException(nameof(workerOptions));
51-
_functionMetadataProvider = functionMetadataProvider ?? throw new ArgumentNullException(nameof(functionMetadataProvider));
49+
_metadataManager = metadataManager ?? throw new ArgumentNullException(nameof(metadataManager));
5250

5351
_invocationHandler = invocationHandler;
5452
}
@@ -152,7 +150,7 @@ private async Task<FunctionMetadataResponse> GetFunctionMetadataAsync(string fun
152150

153151
try
154152
{
155-
var functionMetadataList = await _functionMetadataProvider.GetFunctionMetadataAsync(functionAppDirectory);
153+
var functionMetadataList = await _metadataManager.GetFunctionMetadataAsync(functionAppDirectory);
156154

157155
foreach (var func in functionMetadataList)
158156
{
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.Immutable;
4+
using System.Threading.Tasks;
5+
using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata;
6+
using Microsoft.Extensions.Logging;
7+
using Moq;
8+
using Xunit;
9+
10+
namespace Microsoft.Azure.Functions.Worker.Tests.FunctionMetadata
11+
{
12+
public class DefaultFunctionMetadataManagerTests
13+
{
14+
[Fact]
15+
public async Task GetFunctionMetadataAsync_ReturnsTransformedMetadata()
16+
{
17+
var mockProvider = new Mock<IFunctionMetadataProvider>(MockBehavior.Strict);
18+
var mockTransformer = new Mock<IFunctionMetadataTransformer>(MockBehavior.Strict);
19+
var mockLogger = new Mock<ILogger<DefaultFunctionMetadataManager>>();
20+
21+
var metadata = new List<IFunctionMetadata> { new TestFunctionMetadata() }.ToImmutableArray();
22+
mockProvider.Setup(p => p.GetFunctionMetadataAsync(It.IsAny<string>())).ReturnsAsync(metadata);
23+
24+
mockTransformer.SetupGet(t => t.Name).Returns("TestTransformer");
25+
mockTransformer.Setup(t => t.Transform(It.IsAny<IList<IFunctionMetadata>>()));
26+
27+
var manager = new DefaultFunctionMetadataManager(
28+
mockProvider.Object,
29+
new[] { mockTransformer.Object },
30+
mockLogger.Object);
31+
32+
var result = await manager.GetFunctionMetadataAsync("test");
33+
34+
Assert.Single(result);
35+
mockProvider.Verify(p => p.GetFunctionMetadataAsync("test"), Times.Once);
36+
mockTransformer.Verify(t => t.Transform(It.IsAny<IList<IFunctionMetadata>>()), Times.Once);
37+
}
38+
39+
[Fact]
40+
public async Task GetFunctionMetadataAsync_NoTransformers_ReturnsOriginalMetadata()
41+
{
42+
var mockProvider = new Mock<IFunctionMetadataProvider>(MockBehavior.Strict);
43+
var mockLogger = new Mock<ILogger<DefaultFunctionMetadataManager>>();
44+
var metadata = new List<IFunctionMetadata> { new TestFunctionMetadata() }.ToImmutableArray();
45+
mockProvider.Setup(p => p.GetFunctionMetadataAsync(It.IsAny<string>())).ReturnsAsync(metadata);
46+
47+
var manager = new DefaultFunctionMetadataManager(
48+
mockProvider.Object,
49+
Array.Empty<IFunctionMetadataTransformer>(),
50+
mockLogger.Object);
51+
52+
var result = await manager.GetFunctionMetadataAsync("test");
53+
54+
Assert.Single(result);
55+
mockProvider.Verify(p => p.GetFunctionMetadataAsync("test"), Times.Once);
56+
}
57+
58+
[Fact]
59+
public async Task GetFunctionMetadataAsync_TransformerThrows_LogsAndThrows()
60+
{
61+
var mockProvider = new Mock<IFunctionMetadataProvider>(MockBehavior.Strict);
62+
var mockTransformer = new Mock<IFunctionMetadataTransformer>(MockBehavior.Strict);
63+
var mockLogger = new Mock<ILogger<DefaultFunctionMetadataManager>>();
64+
var metadata = ImmutableArray<IFunctionMetadata>.Empty;
65+
66+
mockProvider.Setup(p => p.GetFunctionMetadataAsync(It.IsAny<string>())).ReturnsAsync(metadata);
67+
mockTransformer.SetupGet(t => t.Name).Returns("ThrowingTransformer");
68+
mockTransformer.Setup(t => t.Transform(It.IsAny<IList<IFunctionMetadata>>()))
69+
.Throws(new InvalidOperationException("fail"));
70+
71+
var manager = new DefaultFunctionMetadataManager(
72+
mockProvider.Object,
73+
new[] { mockTransformer.Object },
74+
mockLogger.Object);
75+
76+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => manager.GetFunctionMetadataAsync("test"));
77+
Assert.Equal("fail", ex.Message);
78+
mockLogger.Verify(l => l.Log(
79+
LogLevel.Error,
80+
It.IsAny<EventId>(),
81+
It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("ThrowingTransformer")),
82+
It.IsAny<Exception>(),
83+
It.IsAny<Func<It.IsAnyType, Exception, string>>()), Times.Once);
84+
}
85+
86+
private class TestFunctionMetadata : IFunctionMetadata
87+
{
88+
public string? FunctionId => "id";
89+
public bool IsProxy => false;
90+
public string? Language => "dotnet";
91+
public bool ManagedDependencyEnabled => false;
92+
public string? Name => "Test";
93+
public string? EntryPoint => "Test.Run";
94+
public IList<string>? RawBindings => new List<string>();
95+
public string? ScriptFile => "Test.dll";
96+
public IRetryOptions? Retry => null;
97+
}
98+
}
99+
}

test/DotNetWorkerTests/GrpcWorkerTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ public async Task Invocation_WhenSynchronous_DoesNotBlock()
279279

280280
var clientFactoryMock = new Mock<IWorkerClientFactory>();
281281
var clientMock = new Mock<IWorkerClient>();
282-
var metadataProvider = new Mock<IFunctionMetadataProvider>();
282+
var metadataManager = new Mock<IFunctionMetadataManager>();
283283
var invocationHandlerMock = new Mock<IInvocationHandler>();
284284

285285
InvocationResponse ValueFunction(InvocationRequest request)
@@ -310,7 +310,7 @@ InvocationResponse ValueFunction(InvocationRequest request)
310310
clientFactoryMock.Object,
311311
_mockMethodInfoLocator.Object,
312312
new OptionsWrapper<WorkerOptions>(new WorkerOptions()),
313-
metadataProvider.Object,
313+
metadataManager.Object,
314314
new ApplicationLifetime(TestLogger<ApplicationLifetime>.Create()),
315315
invocationHandlerMock.Object);
316316

0 commit comments

Comments
 (0)