Skip to content

Commit 018e67c

Browse files
authored
Special case EventGrid sourced blob triggers for grouping (#9987)
* Special case EventGrid sourced blob triggers for grouping * Expand blob group unit test * Update patch version * extract consts
1 parent 01266e7 commit 018e67c

File tree

7 files changed

+163
-38
lines changed

7 files changed

+163
-38
lines changed

build/common.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<LangVersion>latest</LangVersion>
66
<MajorVersion>4</MajorVersion>
77
<MinorVersion>1033</MinorVersion>
8-
<PatchVersion>5</PatchVersion>
8+
<PatchVersion>6</PatchVersion>
99
<BuildNumber Condition="'$(BuildNumber)' == '' ">0</BuildNumber>
1010
<PreviewVersion></PreviewVersion>
1111

src/WebJobs.Script/Description/FunctionGroupListenerDecorator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public IListener Decorate(ListenerDecoratorContext context)
4949
}
5050

5151
string group = functionMetadata.GetFunctionGroup() ?? functionName;
52-
if (string.Equals(targetGroup, group, StringComparison.OrdinalIgnoreCase))
52+
if (FunctionGroups.IsEnabled(targetGroup, group))
5353
{
5454
_logger.LogDebug("Enabling function {functionName}", functionName);
5555
return context.Listener;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
6+
namespace Microsoft.Azure.WebJobs.Script.Description
7+
{
8+
public static class FunctionGroups
9+
{
10+
public const string Http = "http";
11+
public const string Durable = "durable";
12+
public const string Blob = "blob";
13+
14+
public static string ForFunction(string function)
15+
{
16+
return $"function:{function}";
17+
}
18+
19+
public static bool IsEnabled(string targetGroup, string triggerGroup)
20+
{
21+
if (string.Equals(targetGroup, triggerGroup, StringComparison.OrdinalIgnoreCase))
22+
{
23+
return true;
24+
}
25+
26+
if (string.Equals(targetGroup, Http, StringComparison.OrdinalIgnoreCase)
27+
&& string.Equals(triggerGroup, Blob, StringComparison.OrdinalIgnoreCase))
28+
{
29+
// The blob group needs to be special cased as it is a two step process:
30+
// 1. WebHook which enqueues a message to Azure Storage Queue.
31+
// 2. Azure Storage Queue trigger which processes the message.
32+
// So we need to run in both http and blob groups.
33+
return true;
34+
}
35+
36+
return false;
37+
}
38+
}
39+
}

src/WebJobs.Script/Extensions/BindingMetadataExtensions.cs

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ namespace Microsoft.Azure.WebJobs.Script.Extensions
1010
{
1111
public static class BindingMetadataExtensions
1212
{
13-
private const string HttpTriggerKey = "httpTrigger";
14-
private const string EventGridTriggerKey = "eventGridTrigger";
15-
private const string SignalRTriggerKey = "signalRTrigger";
16-
private const string BlobTriggerKey = "blobTrigger";
13+
private const string HttpTrigger = "httpTrigger";
14+
private const string EventGridTrigger = "eventGridTrigger";
15+
private const string SignalRTrigger = "signalRTrigger";
16+
private const string BlobTrigger = "blobTrigger";
17+
18+
private const string BlobSourceKey = "source";
19+
private const string EventGridSource = "eventGrid";
1720

1821
private static readonly HashSet<string> DurableTriggers = new(StringComparer.OrdinalIgnoreCase)
1922
{
@@ -34,7 +37,7 @@ public static bool IsHttpTrigger(this BindingMetadata binding)
3437
throw new ArgumentNullException(nameof(binding));
3538
}
3639

37-
return string.Equals(HttpTriggerKey, binding.Type, StringComparison.OrdinalIgnoreCase);
40+
return string.Equals(HttpTrigger, binding.Type, StringComparison.OrdinalIgnoreCase);
3841
}
3942

4043
/// <summary>
@@ -43,7 +46,7 @@ public static bool IsHttpTrigger(this BindingMetadata binding)
4346
/// <param name="binding">The binding metadata to check.</param>
4447
/// <returns><c>true</c> if a webhook trigger, <c>false</c> otherwise.</returns>
4548
/// <remarks>
46-
/// Known webhook triggers includes SignalR, Event Grid and Event Grid sourced blob triggers.
49+
/// Known webhook triggers includes SignalR, Event Grid triggers.
4750
/// </remarks>
4851
public static bool IsWebHookTrigger(this BindingMetadata binding)
4952
{
@@ -52,23 +55,12 @@ public static bool IsWebHookTrigger(this BindingMetadata binding)
5255
throw new ArgumentNullException(nameof(binding));
5356
}
5457

55-
if (string.Equals(EventGridTriggerKey, binding.Type, StringComparison.OrdinalIgnoreCase)
56-
|| string.Equals(SignalRTriggerKey, binding.Type, StringComparison.OrdinalIgnoreCase))
58+
if (string.Equals(EventGridTrigger, binding.Type, StringComparison.OrdinalIgnoreCase)
59+
|| string.Equals(SignalRTrigger, binding.Type, StringComparison.OrdinalIgnoreCase))
5760
{
5861
return true;
5962
}
6063

61-
if (string.Equals(BlobTriggerKey, binding.Type, StringComparison.OrdinalIgnoreCase))
62-
{
63-
if (binding.Raw is { } obj)
64-
{
65-
if (obj.TryGetValue("source", StringComparison.OrdinalIgnoreCase, out JToken token) && token is not null)
66-
{
67-
return string.Equals(token.ToString(), "eventGrid", StringComparison.OrdinalIgnoreCase);
68-
}
69-
}
70-
}
71-
7264
return false;
7365
}
7466

@@ -87,6 +79,32 @@ public static bool IsDurableTrigger(this BindingMetadata binding)
8779
return DurableTriggers.Contains(binding.Type);
8880
}
8981

82+
/// <summary>
83+
/// Checks if a <see cref="BindingMetadata"/> represents an EventGrid sourced blob trigger.
84+
/// </summary>
85+
/// <param name="binding">The binding metadata to check.</param>
86+
/// <returns><c>true</c> if a EventGrid sourced blob trigger, <c>false</c> otherwise.</returns>
87+
public static bool IsEventGridBlobTrigger(this BindingMetadata binding)
88+
{
89+
if (binding is null)
90+
{
91+
throw new ArgumentNullException(nameof(binding));
92+
}
93+
94+
if (string.Equals(BlobTrigger, binding.Type, StringComparison.OrdinalIgnoreCase))
95+
{
96+
if (binding.Raw is { } obj)
97+
{
98+
if (obj.TryGetValue(BlobSourceKey, StringComparison.OrdinalIgnoreCase, out JToken token) && token is not null)
99+
{
100+
return string.Equals(token.ToString(), EventGridSource, StringComparison.OrdinalIgnoreCase);
101+
}
102+
}
103+
}
104+
105+
return false;
106+
}
107+
90108
public static bool SupportsDeferredBinding(this BindingMetadata metadata)
91109
{
92110
Utility.TryReadAsBool(metadata.Properties, ScriptConstants.SupportsDeferredBindingKey, out bool result);

src/WebJobs.Script/Extensions/FunctionMetadataExtensions.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,17 +78,22 @@ public static string GetFunctionGroup(this FunctionMetadata metadata)
7878
{
7979
if (binding.IsHttpTrigger() || binding.IsWebHookTrigger())
8080
{
81-
return "http";
81+
return FunctionGroups.Http;
8282
}
8383

8484
if (binding.IsDurableTrigger())
8585
{
86-
return "durable";
86+
return FunctionGroups.Durable;
87+
}
88+
89+
if (binding.IsEventGridBlobTrigger())
90+
{
91+
return FunctionGroups.Blob;
8792
}
8893
}
8994

9095
// A function with no specified group will be assigned to a group of itself.
91-
return $"function:{metadata.Name}";
96+
return FunctionGroups.ForFunction(metadata.Name);
9297
}
9398

9499
public static string GetFunctionId(this FunctionMetadata metadata)

test/WebJobs.Script.Tests/Description/FunctionGroupListenerDecoratorTests.cs

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public void Decorate_NoTargetGroupConfigured_ReturnsOriginalListener()
3131
IFunctionDefinition definition = Mock.Of<IFunctionDefinition>();
3232
IListener original = Mock.Of<IListener>();
3333
IFunctionMetadataManager metadata = Mock.Of<IFunctionMetadataManager>();
34-
IEnvironment environment = CreateEnvironment(FuncGroup.None);
34+
IEnvironment environment = CreateEnvironment(null);
3535

3636
var context = new ListenerDecoratorContext(definition, original.GetType(), original);
3737
var decorator = new FunctionGroupListenerDecorator(metadata, environment, _logger);
@@ -50,7 +50,7 @@ public void Decorate_MetadataNotFound_ReturnsOriginalListener()
5050
IFunctionDefinition definition = CreateDefinition("test");
5151
IListener original = Mock.Of<IListener>();
5252
IFunctionMetadataManager metadata = Mock.Of<IFunctionMetadataManager>();
53-
IEnvironment environment = CreateEnvironment(FuncGroup.Http);
53+
IEnvironment environment = CreateEnvironment("http");
5454

5555
var context = new ListenerDecoratorContext(definition, original.GetType(), original);
5656
var decorator = new FunctionGroupListenerDecorator(metadata, environment, _logger);
@@ -69,7 +69,7 @@ public void Decorate_GroupMatch_ReturnsOriginalListener()
6969
IFunctionDefinition definition = CreateDefinition("test");
7070
IListener original = Mock.Of<IListener>();
7171
IFunctionMetadataManager metadata = CreateMetadataManager("test", true);
72-
IEnvironment environment = CreateEnvironment(FuncGroup.Http);
72+
IEnvironment environment = CreateEnvironment("http");
7373

7474
var context = new ListenerDecoratorContext(definition, original.GetType(), original);
7575
var decorator = new FunctionGroupListenerDecorator(metadata, environment, _logger);
@@ -88,7 +88,7 @@ public void Decorate_GroupDoesNotMatch_ReturnsNoOpListener()
8888
IFunctionDefinition definition = CreateDefinition("test");
8989
IListener original = Mock.Of<IListener>();
9090
IFunctionMetadataManager metadata = CreateMetadataManager("test", true);
91-
IEnvironment environment = CreateEnvironment(FuncGroup.Other);
91+
IEnvironment environment = CreateEnvironment("other");
9292

9393
var context = new ListenerDecoratorContext(definition, original.GetType(), original);
9494
var decorator = new FunctionGroupListenerDecorator(metadata, environment, _logger);
@@ -100,6 +100,51 @@ public void Decorate_GroupDoesNotMatch_ReturnsNoOpListener()
100100
Assert.NotSame(context.Listener, result);
101101
}
102102

103+
[Theory]
104+
[InlineData("http")]
105+
[InlineData("blob")]
106+
public void Decorate_BlobTrigger_EnabledForGroup(string targetGroup)
107+
{
108+
// Arrange
109+
IFunctionDefinition definition = CreateDefinition("test");
110+
IListener original = Mock.Of<IListener>();
111+
112+
var metadata = new FunctionMetadata()
113+
{
114+
Name = "TestFunction1",
115+
Bindings =
116+
{
117+
new BindingMetadata
118+
{
119+
Name = "input",
120+
Type = "blobTrigger",
121+
Direction = BindingDirection.In,
122+
Raw = new JObject()
123+
{
124+
["name"] = "input",
125+
["type"] = "blobTrigger",
126+
["direction"] = "in",
127+
["source"] = "EventGrid",
128+
},
129+
}
130+
}
131+
};
132+
133+
var metadataMock = new Mock<IFunctionMetadataManager>();
134+
metadataMock.Setup(p => p.TryGetFunctionMetadata("test", out metadata, false)).Returns(true);
135+
136+
IEnvironment environment = CreateEnvironment(targetGroup);
137+
138+
var context = new ListenerDecoratorContext(definition, original.GetType(), original);
139+
var decorator = new FunctionGroupListenerDecorator(metadataMock.Object, environment, _logger);
140+
141+
// Act
142+
var result = decorator.Decorate(context);
143+
144+
// Assert
145+
Assert.Same(context.Listener, result);
146+
}
147+
103148
private static IFunctionDefinition CreateDefinition(string name)
104149
{
105150
var descriptor = new FuncDescriptor { LogName = name };
@@ -134,17 +179,10 @@ private static IFunctionMetadataManager CreateMetadataManager(string name, bool
134179
return mock.Object;
135180
}
136181

137-
private static IEnvironment CreateEnvironment(FuncGroup group)
182+
private static IEnvironment CreateEnvironment(string group)
138183
{
139-
string groupStr = group switch
140-
{
141-
FuncGroup.Http => "http",
142-
FuncGroup.Other => "other",
143-
_ => null,
144-
};
145-
146184
var environment = new Mock<IEnvironment>(MockBehavior.Strict);
147-
environment.Setup(p => p.GetEnvironmentVariable(EnvironmentSettingNames.FunctionsTargetGroup)).Returns(groupStr);
185+
environment.Setup(p => p.GetEnvironmentVariable(EnvironmentSettingNames.FunctionsTargetGroup)).Returns(group);
148186
return environment.Object;
149187
}
150188
}

test/WebJobs.Script.Tests/Extensions/BindingMetadataExtensionsTests.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public void IsHttpTrigger_ReturnsExpectedValue(string type, bool expected)
7373
[Theory]
7474
[InlineData("eventGridTrigger", true)]
7575
[InlineData("signalRTrigger", true)]
76-
[InlineData("blobTrigger", true, "eventGrid")]
76+
[InlineData("blobTrigger", false, "eventGrid")]
7777
[InlineData("blobTrigger", false, "other")]
7878
[InlineData("httpTrigger", false)]
7979
[InlineData("inputBinding", false)]
@@ -110,5 +110,30 @@ public void IsDurableTrigger_ReturnsExpectedValue(string type, bool expected)
110110

111111
Assert.Equal(expected, bindingMetadata.IsDurableTrigger());
112112
}
113+
114+
[Theory]
115+
[InlineData("blobTrigger", true, "eventGrid")]
116+
[InlineData("blobTrigger", false, "other")]
117+
[InlineData("blobTrigger", false, null)]
118+
[InlineData("otherTrigger", false, "eventGrid")]
119+
[InlineData("otherTrigger", false, "other")]
120+
[InlineData("otherTrigger", false, null)]
121+
public void IsEventGridBlobTrigger_ReturnsExpectedValue(string type, bool expected, string source)
122+
{
123+
var bindingMetadata = new BindingMetadata
124+
{
125+
Type = type,
126+
};
127+
128+
if (source is not null)
129+
{
130+
bindingMetadata.Raw = new JObject
131+
{
132+
["source"] = source,
133+
};
134+
}
135+
136+
Assert.Equal(expected, bindingMetadata.IsEventGridBlobTrigger());
137+
}
113138
}
114139
}

0 commit comments

Comments
 (0)