Skip to content

Commit dba54bd

Browse files
committed
Expose middleware properties in activity/orchestrator
The properties bag from DispatchMiddlewareContext contains information that might be useful from activities/orchestrators. However, they currently do not show up from those context options. This change plumbs the dictionary through and surfaces it for both activities and orchestrators. As part of this, a new interface IContextProperties is added that just has the properties dictionary. However, this allows for shared code to set and get properties by type or by name.
1 parent 54436c6 commit dba54bd

13 files changed

+356
-21
lines changed
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// ----------------------------------------------------------------------------------
2+
// Copyright Microsoft Corporation
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS,
9+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
// See the License for the specific language governing permissions and
11+
// limitations under the License.
12+
// ----------------------------------------------------------------------------------
13+
#nullable enable
14+
namespace DurableTask.Core.Tests
15+
{
16+
using System;
17+
using System.Diagnostics;
18+
using System.Threading.Tasks;
19+
using DurableTask.Emulator;
20+
using Microsoft.VisualStudio.TestTools.UnitTesting;
21+
22+
[TestClass]
23+
public class PropertiesMiddlewareTests
24+
{
25+
private const string PropertyKey = "Test";
26+
private const string PropertyValue = "Value";
27+
28+
TaskHubWorker worker = null!;
29+
TaskHubClient client = null!;
30+
31+
[TestInitialize]
32+
public async Task Initialize()
33+
{
34+
var service = new LocalOrchestrationService();
35+
this.worker = new TaskHubWorker(service);
36+
37+
await this.worker
38+
.AddTaskOrchestrations(typeof(NoActivities), typeof(RunActivityOrchestrator))
39+
.AddTaskActivities(typeof(ReturnPropertyActivity))
40+
.StartAsync();
41+
42+
this.client = new TaskHubClient(service);
43+
}
44+
45+
[TestCleanup]
46+
public async Task TestCleanup()
47+
{
48+
await this.worker.StopAsync(true);
49+
}
50+
51+
private sealed class NoActivities : TaskOrchestration<string, string>
52+
{
53+
public override Task<string> RunTask(OrchestrationContext context, string input)
54+
{
55+
return Task.FromResult(context.GetProperty<string>(PropertyKey)!);
56+
}
57+
}
58+
59+
private sealed class ReturnPropertyActivity : TaskActivity<string, string>
60+
{
61+
protected override string Execute(TaskContext context, string input)
62+
{
63+
return context.GetProperty<string>(PropertyKey)!;
64+
}
65+
}
66+
67+
private sealed class RunActivityOrchestrator : TaskOrchestration<string, string>
68+
{
69+
public override Task<string> RunTask(OrchestrationContext context, string input)
70+
{
71+
return context.ScheduleTask<string>(typeof(ReturnPropertyActivity));
72+
}
73+
}
74+
75+
[TestMethod]
76+
public async Task OrchestrationGetsProperties()
77+
{
78+
this.worker.AddOrchestrationDispatcherMiddleware((context, next) =>
79+
{
80+
context.SetProperty<string>(PropertyKey, PropertyValue);
81+
82+
return next();
83+
});
84+
85+
OrchestrationInstance instance = await this.client.CreateOrchestrationInstanceAsync(typeof(NoActivities), null);
86+
87+
TimeSpan timeout = TimeSpan.FromSeconds(Debugger.IsAttached ? 1000 : 10);
88+
var state = await this.client.WaitForOrchestrationAsync(instance, timeout);
89+
90+
Assert.AreEqual($"\"{PropertyValue}\"", state.Output);
91+
}
92+
93+
[TestMethod]
94+
public async Task OrchestrationDoesNotGetPropertiesFromActivityMiddleware()
95+
{
96+
this.worker.AddActivityDispatcherMiddleware((context, next) =>
97+
{
98+
context.SetProperty<string>(PropertyKey, PropertyValue);
99+
100+
return next();
101+
});
102+
103+
OrchestrationInstance instance = await this.client.CreateOrchestrationInstanceAsync(typeof(NoActivities), null);
104+
105+
TimeSpan timeout = TimeSpan.FromSeconds(Debugger.IsAttached ? 1000 : 10);
106+
var state = await this.client.WaitForOrchestrationAsync(instance, timeout);
107+
108+
Assert.IsNull(state.Output);
109+
}
110+
111+
[TestMethod]
112+
public async Task ActivityGetsProperties()
113+
{
114+
this.worker.AddActivityDispatcherMiddleware((context, next) =>
115+
{
116+
context.SetProperty<string>(PropertyKey, PropertyValue);
117+
118+
return next();
119+
});
120+
121+
OrchestrationInstance instance = await this.client.CreateOrchestrationInstanceAsync(typeof(RunActivityOrchestrator), null);
122+
123+
TimeSpan timeout = TimeSpan.FromSeconds(Debugger.IsAttached ? 1000 : 10);
124+
var state = await this.client.WaitForOrchestrationAsync(instance, timeout);
125+
126+
Assert.AreEqual($"\"{PropertyValue}\"", state.Output);
127+
}
128+
129+
[TestMethod]
130+
public async Task ActivityDoesNotGetPropertiesFromOrchestratorMiddleware()
131+
{
132+
this.worker.AddOrchestrationDispatcherMiddleware((context, next) =>
133+
{
134+
context.SetProperty<string>(PropertyKey, PropertyValue);
135+
136+
return next();
137+
});
138+
139+
OrchestrationInstance instance = await this.client.CreateOrchestrationInstanceAsync(typeof(RunActivityOrchestrator), null);
140+
141+
TimeSpan timeout = TimeSpan.FromSeconds(Debugger.IsAttached ? 1000 : 10);
142+
var state = await this.client.WaitForOrchestrationAsync(instance, timeout);
143+
144+
Assert.IsNull(state.Output);
145+
}
146+
}
147+
}

Test/DurableTask.Core.Tests/RetryInterceptorTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ sealed class MockOrchestrationContext : TaskOrchestrationContext
8989
readonly List<TimeSpan> delays = new List<TimeSpan>();
9090

9191
public MockOrchestrationContext(OrchestrationInstance orchestrationInstance, TaskScheduler taskScheduler)
92-
: base(orchestrationInstance, taskScheduler)
92+
: base(orchestrationInstance, new PropertiesDictionary(), taskScheduler)
9393
{
9494
CurrentUtcDateTime = DateTime.UtcNow;
9595
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// ----------------------------------------------------------------------------------
2+
// Copyright Microsoft Corporation
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS,
9+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
// See the License for the specific language governing permissions and
11+
// limitations under the License.
12+
// ----------------------------------------------------------------------------------
13+
14+
#nullable enable
15+
16+
using System;
17+
using System.Collections.Generic;
18+
19+
namespace DurableTask.Core
20+
{
21+
/// <summary>
22+
/// Extension methods that help get properties from <see cref="IContextProperties"/>.
23+
/// </summary>
24+
public static class ContextPropertiesExtensions
25+
{
26+
/// <summary>
27+
/// Sets a property value to the context using the full name of the type as the key.
28+
/// </summary>
29+
/// <typeparam name="T">The type of the property.</typeparam>
30+
/// <param name="properties">Properties to set property for.</param>
31+
/// <param name="value">The value of the property.</param>
32+
public static void SetProperty<T>(this IContextProperties properties, T? value) => properties.SetProperty(typeof(T).FullName, value);
33+
34+
/// <summary>
35+
/// Sets a named property value to the context.
36+
/// </summary>
37+
/// <typeparam name="T">The type of the property.</typeparam>
38+
/// <param name="properties">Properties to set property for.</param>
39+
/// <param name="key">The name of the property.</param>
40+
/// <param name="value">The value of the property.</param>
41+
public static void SetProperty<T>(this IContextProperties properties, string key, T? value)
42+
{
43+
if (value is null)
44+
{
45+
properties.Properties.Remove(key);
46+
}
47+
else
48+
{
49+
properties.Properties[key] = value;
50+
}
51+
}
52+
53+
/// <summary>
54+
/// Gets a property value from the context using the full name of <typeparamref name="T"/>.
55+
/// </summary>
56+
/// <typeparam name="T">The type of the property.</typeparam>
57+
/// <param name="properties">Properties to get property from.</param>
58+
/// <returns>The value of the property or <c>default(T)</c> if the property is not defined.</returns>
59+
public static T? GetProperty<T>(this IContextProperties properties) => properties.GetProperty<T>(typeof(T).FullName);
60+
61+
internal static T GetRequiredProperty<T>(this IContextProperties properties)
62+
=> properties.GetProperty<T>() ?? throw new InvalidOperationException($"Could not find property for {typeof(T).FullName}");
63+
64+
/// <summary>
65+
/// Gets a named property value from the context.
66+
/// </summary>
67+
/// <typeparam name="T"></typeparam>
68+
/// <param name="properties">Properties to get property from.</param>
69+
/// <param name="key">The name of the property value.</param>
70+
/// <returns>The value of the property or <c>default(T)</c> if the property is not defined.</returns>
71+
public static T? GetProperty<T>(this IContextProperties properties, string key) => properties.Properties.TryGetValue(key, out object value) ? (T)value : default;
72+
73+
/// <summary>
74+
/// Gets the tags from the current properties.
75+
/// </summary>
76+
/// <param name="properties"></param>
77+
/// <returns></returns>
78+
public static IDictionary<string, string> GetTags(this IContextProperties properties) => properties.GetRequiredProperty<OrchestrationExecutionContext>().OrchestrationTags;
79+
}
80+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// ----------------------------------------------------------------------------------
2+
// Copyright Microsoft Corporation
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS,
9+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
// See the License for the specific language governing permissions and
11+
// limitations under the License.
12+
// ----------------------------------------------------------------------------------
13+
14+
using System.Collections.Generic;
15+
16+
#nullable enable
17+
18+
namespace DurableTask.Core
19+
{
20+
/// <summary>
21+
/// Collection of properties for context objects to store arbitrary state.
22+
/// </summary>
23+
public interface IContextProperties
24+
{
25+
/// <summary>
26+
/// Gets the properties of the current instance
27+
/// </summary>
28+
IDictionary<string, object> Properties { get; }
29+
}
30+
}

src/DurableTask.Core/Middleware/DispatchMiddlewareContext.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,12 @@
1313

1414
namespace DurableTask.Core.Middleware
1515
{
16-
using System;
1716
using System.Collections.Generic;
1817

1918
/// <summary>
2019
/// Context data that can be used to share data between middleware.
2120
/// </summary>
22-
public class DispatchMiddlewareContext
21+
public class DispatchMiddlewareContext : IContextProperties
2322
{
2423
/// <summary>
2524
/// Sets a property value to the context using the full name of the type as the key.
@@ -28,7 +27,7 @@ public class DispatchMiddlewareContext
2827
/// <param name="value">The value of the property.</param>
2928
public void SetProperty<T>(T value)
3029
{
31-
SetProperty(typeof(T).FullName, value);
30+
ContextPropertiesExtensions.SetProperty(this, value);
3231
}
3332

3433
/// <summary>
@@ -39,7 +38,7 @@ public void SetProperty<T>(T value)
3938
/// <param name="value">The value of the property.</param>
4039
public void SetProperty<T>(string key, T value)
4140
{
42-
Properties[key] = value;
41+
ContextPropertiesExtensions.SetProperty(this, key, value);
4342
}
4443

4544
/// <summary>
@@ -49,7 +48,7 @@ public void SetProperty<T>(string key, T value)
4948
/// <returns>The value of the property or <c>default(T)</c> if the property is not defined.</returns>
5049
public T GetProperty<T>()
5150
{
52-
return GetProperty<T>(typeof(T).FullName);
51+
return ContextPropertiesExtensions.GetProperty<T>(this);
5352
}
5453

5554
/// <summary>
@@ -60,12 +59,12 @@ public T GetProperty<T>()
6059
/// <returns>The value of the property or <c>default(T)</c> if the property is not defined.</returns>
6160
public T GetProperty<T>(string key)
6261
{
63-
return Properties.TryGetValue(key, out object value) ? (T)value : default(T);
62+
return ContextPropertiesExtensions.GetProperty<T>(this, key);
6463
}
6564

6665
/// <summary>
6766
/// Gets a key/value collection that can be used to share data between middleware.
6867
/// </summary>
69-
public IDictionary<string, object> Properties { get; } = new Dictionary<string, object>(StringComparer.Ordinal);
68+
public IDictionary<string, object> Properties { get; } = new PropertiesDictionary();
7069
}
7170
}

src/DurableTask.Core/OrchestrationContext.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,16 @@ namespace DurableTask.Core
2323
/// <summary>
2424
/// Context for an orchestration containing the instance, replay status, orchestration methods and proxy methods
2525
/// </summary>
26-
public abstract class OrchestrationContext
26+
public abstract class OrchestrationContext : IContextProperties
2727
{
2828
/// <summary>
2929
/// Used in generating proxy interfaces and classes.
3030
/// </summary>
3131
private static readonly ProxyGenerator ProxyGenerator = new ProxyGenerator();
3232

33+
/// <inheritdoc/>
34+
public virtual IDictionary<string, object> Properties { get; } = new Dictionary<string, object>();
35+
3336
/// <summary>
3437
/// Thread-static variable used to signal whether the calling thread is the orchestrator thread.
3538
/// The primary use case is for detecting illegal async usage in orchestration code.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// ----------------------------------------------------------------------------------
2+
// Copyright Microsoft Corporation
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS,
9+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
// See the License for the specific language governing permissions and
11+
// limitations under the License.
12+
// ----------------------------------------------------------------------------------
13+
14+
namespace DurableTask.Core
15+
{
16+
using System;
17+
using System.Collections.Generic;
18+
19+
internal sealed class PropertiesDictionary : Dictionary<string, object>, IContextProperties
20+
{
21+
public PropertiesDictionary()
22+
: base(StringComparer.Ordinal)
23+
{
24+
}
25+
26+
IDictionary<string, object> IContextProperties.Properties => this;
27+
}
28+
}

src/DurableTask.Core/TaskActivityDispatcher.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ async Task OnProcessWorkItemAsync(TaskActivityWorkItem workItem)
174174
ActivityExecutionResult? result;
175175
try
176176
{
177-
await this.dispatchPipeline.RunAsync(dispatchContext, async _ =>
177+
await this.dispatchPipeline.RunAsync(dispatchContext, async dispatchContext =>
178178
{
179179
if (taskActivity == null)
180180
{
@@ -185,7 +185,7 @@ await this.dispatchPipeline.RunAsync(dispatchContext, async _ =>
185185
throw new TypeMissingException($"TaskActivity {scheduledEvent.Name} version {scheduledEvent.Version} was not found");
186186
}
187187

188-
var context = new TaskContext(taskMessage.OrchestrationInstance);
188+
var context = new TaskContext(taskMessage.OrchestrationInstance, dispatchContext.Properties);
189189
context.ErrorPropagationMode = this.errorPropagationMode;
190190

191191
HistoryEvent? responseEvent;

0 commit comments

Comments
 (0)