Skip to content

Commit

Permalink
Expose middleware properties in activity/orchestrator
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
twsouthwick committed Sep 8, 2023
1 parent 54436c6 commit dba54bd
Show file tree
Hide file tree
Showing 13 changed files with 356 additions and 21 deletions.
147 changes: 147 additions & 0 deletions Test/DurableTask.Core.Tests/PropertiesMiddlewareTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// ----------------------------------------------------------------------------------
// Copyright Microsoft Corporation
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ----------------------------------------------------------------------------------
#nullable enable
namespace DurableTask.Core.Tests
{
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using DurableTask.Emulator;
using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class PropertiesMiddlewareTests
{
private const string PropertyKey = "Test";
private const string PropertyValue = "Value";

TaskHubWorker worker = null!;
TaskHubClient client = null!;

[TestInitialize]
public async Task Initialize()
{
var service = new LocalOrchestrationService();
this.worker = new TaskHubWorker(service);

await this.worker
.AddTaskOrchestrations(typeof(NoActivities), typeof(RunActivityOrchestrator))
.AddTaskActivities(typeof(ReturnPropertyActivity))
.StartAsync();

this.client = new TaskHubClient(service);
}

[TestCleanup]
public async Task TestCleanup()
{
await this.worker.StopAsync(true);
}

private sealed class NoActivities : TaskOrchestration<string, string>
{
public override Task<string> RunTask(OrchestrationContext context, string input)
{
return Task.FromResult(context.GetProperty<string>(PropertyKey)!);
}
}

private sealed class ReturnPropertyActivity : TaskActivity<string, string>
{
protected override string Execute(TaskContext context, string input)
{
return context.GetProperty<string>(PropertyKey)!;
}
}

private sealed class RunActivityOrchestrator : TaskOrchestration<string, string>
{
public override Task<string> RunTask(OrchestrationContext context, string input)
{
return context.ScheduleTask<string>(typeof(ReturnPropertyActivity));
}
}

[TestMethod]
public async Task OrchestrationGetsProperties()
{
this.worker.AddOrchestrationDispatcherMiddleware((context, next) =>
{
context.SetProperty<string>(PropertyKey, PropertyValue);
return next();
});

OrchestrationInstance instance = await this.client.CreateOrchestrationInstanceAsync(typeof(NoActivities), null);

TimeSpan timeout = TimeSpan.FromSeconds(Debugger.IsAttached ? 1000 : 10);
var state = await this.client.WaitForOrchestrationAsync(instance, timeout);

Assert.AreEqual($"\"{PropertyValue}\"", state.Output);
}

[TestMethod]
public async Task OrchestrationDoesNotGetPropertiesFromActivityMiddleware()
{
this.worker.AddActivityDispatcherMiddleware((context, next) =>
{
context.SetProperty<string>(PropertyKey, PropertyValue);
return next();
});

OrchestrationInstance instance = await this.client.CreateOrchestrationInstanceAsync(typeof(NoActivities), null);

TimeSpan timeout = TimeSpan.FromSeconds(Debugger.IsAttached ? 1000 : 10);
var state = await this.client.WaitForOrchestrationAsync(instance, timeout);

Assert.IsNull(state.Output);
}

[TestMethod]
public async Task ActivityGetsProperties()
{
this.worker.AddActivityDispatcherMiddleware((context, next) =>
{
context.SetProperty<string>(PropertyKey, PropertyValue);
return next();
});

OrchestrationInstance instance = await this.client.CreateOrchestrationInstanceAsync(typeof(RunActivityOrchestrator), null);

TimeSpan timeout = TimeSpan.FromSeconds(Debugger.IsAttached ? 1000 : 10);
var state = await this.client.WaitForOrchestrationAsync(instance, timeout);

Assert.AreEqual($"\"{PropertyValue}\"", state.Output);
}

[TestMethod]
public async Task ActivityDoesNotGetPropertiesFromOrchestratorMiddleware()
{
this.worker.AddOrchestrationDispatcherMiddleware((context, next) =>
{
context.SetProperty<string>(PropertyKey, PropertyValue);
return next();
});

OrchestrationInstance instance = await this.client.CreateOrchestrationInstanceAsync(typeof(RunActivityOrchestrator), null);

TimeSpan timeout = TimeSpan.FromSeconds(Debugger.IsAttached ? 1000 : 10);
var state = await this.client.WaitForOrchestrationAsync(instance, timeout);

Assert.IsNull(state.Output);
}
}
}
2 changes: 1 addition & 1 deletion Test/DurableTask.Core.Tests/RetryInterceptorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ sealed class MockOrchestrationContext : TaskOrchestrationContext
readonly List<TimeSpan> delays = new List<TimeSpan>();

public MockOrchestrationContext(OrchestrationInstance orchestrationInstance, TaskScheduler taskScheduler)
: base(orchestrationInstance, taskScheduler)
: base(orchestrationInstance, new PropertiesDictionary(), taskScheduler)
{
CurrentUtcDateTime = DateTime.UtcNow;
}
Expand Down
80 changes: 80 additions & 0 deletions src/DurableTask.Core/ContextPropertiesExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// ----------------------------------------------------------------------------------
// Copyright Microsoft Corporation
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ----------------------------------------------------------------------------------

#nullable enable

using System;
using System.Collections.Generic;

namespace DurableTask.Core
{
/// <summary>
/// Extension methods that help get properties from <see cref="IContextProperties"/>.
/// </summary>
public static class ContextPropertiesExtensions
{
/// <summary>
/// Sets a property value to the context using the full name of the type as the key.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="properties">Properties to set property for.</param>
/// <param name="value">The value of the property.</param>
public static void SetProperty<T>(this IContextProperties properties, T? value) => properties.SetProperty(typeof(T).FullName, value);

/// <summary>
/// Sets a named property value to the context.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="properties">Properties to set property for.</param>
/// <param name="key">The name of the property.</param>
/// <param name="value">The value of the property.</param>
public static void SetProperty<T>(this IContextProperties properties, string key, T? value)
{
if (value is null)
{
properties.Properties.Remove(key);
}
else
{
properties.Properties[key] = value;
}
}

/// <summary>
/// Gets a property value from the context using the full name of <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="properties">Properties to get property from.</param>
/// <returns>The value of the property or <c>default(T)</c> if the property is not defined.</returns>
public static T? GetProperty<T>(this IContextProperties properties) => properties.GetProperty<T>(typeof(T).FullName);

internal static T GetRequiredProperty<T>(this IContextProperties properties)
=> properties.GetProperty<T>() ?? throw new InvalidOperationException($"Could not find property for {typeof(T).FullName}");

/// <summary>
/// Gets a named property value from the context.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="properties">Properties to get property from.</param>
/// <param name="key">The name of the property value.</param>
/// <returns>The value of the property or <c>default(T)</c> if the property is not defined.</returns>
public static T? GetProperty<T>(this IContextProperties properties, string key) => properties.Properties.TryGetValue(key, out object value) ? (T)value : default;

/// <summary>
/// Gets the tags from the current properties.
/// </summary>
/// <param name="properties"></param>
/// <returns></returns>
public static IDictionary<string, string> GetTags(this IContextProperties properties) => properties.GetRequiredProperty<OrchestrationExecutionContext>().OrchestrationTags;
}
}
30 changes: 30 additions & 0 deletions src/DurableTask.Core/IContextProperties.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// ----------------------------------------------------------------------------------
// Copyright Microsoft Corporation
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ----------------------------------------------------------------------------------

using System.Collections.Generic;

#nullable enable

namespace DurableTask.Core
{
/// <summary>
/// Collection of properties for context objects to store arbitrary state.
/// </summary>
public interface IContextProperties
{
/// <summary>
/// Gets the properties of the current instance
/// </summary>
IDictionary<string, object> Properties { get; }
}
}
13 changes: 6 additions & 7 deletions src/DurableTask.Core/Middleware/DispatchMiddlewareContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,12 @@

namespace DurableTask.Core.Middleware
{
using System;
using System.Collections.Generic;

/// <summary>
/// Context data that can be used to share data between middleware.
/// </summary>
public class DispatchMiddlewareContext
public class DispatchMiddlewareContext : IContextProperties
{
/// <summary>
/// Sets a property value to the context using the full name of the type as the key.
Expand All @@ -28,7 +27,7 @@ public class DispatchMiddlewareContext
/// <param name="value">The value of the property.</param>
public void SetProperty<T>(T value)
{
SetProperty(typeof(T).FullName, value);
ContextPropertiesExtensions.SetProperty(this, value);
}

/// <summary>
Expand All @@ -39,7 +38,7 @@ public void SetProperty<T>(T value)
/// <param name="value">The value of the property.</param>
public void SetProperty<T>(string key, T value)
{
Properties[key] = value;
ContextPropertiesExtensions.SetProperty(this, key, value);
}

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

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

/// <summary>
/// Gets a key/value collection that can be used to share data between middleware.
/// </summary>
public IDictionary<string, object> Properties { get; } = new Dictionary<string, object>(StringComparer.Ordinal);
public IDictionary<string, object> Properties { get; } = new PropertiesDictionary();
}
}
5 changes: 4 additions & 1 deletion src/DurableTask.Core/OrchestrationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@ namespace DurableTask.Core
/// <summary>
/// Context for an orchestration containing the instance, replay status, orchestration methods and proxy methods
/// </summary>
public abstract class OrchestrationContext
public abstract class OrchestrationContext : IContextProperties
{
/// <summary>
/// Used in generating proxy interfaces and classes.
/// </summary>
private static readonly ProxyGenerator ProxyGenerator = new ProxyGenerator();

/// <inheritdoc/>
public virtual IDictionary<string, object> Properties { get; } = new Dictionary<string, object>();

/// <summary>
/// Thread-static variable used to signal whether the calling thread is the orchestrator thread.
/// The primary use case is for detecting illegal async usage in orchestration code.
Expand Down
28 changes: 28 additions & 0 deletions src/DurableTask.Core/PropertiesDictionary.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// ----------------------------------------------------------------------------------
// Copyright Microsoft Corporation
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ----------------------------------------------------------------------------------

namespace DurableTask.Core
{
using System;
using System.Collections.Generic;

internal sealed class PropertiesDictionary : Dictionary<string, object>, IContextProperties
{
public PropertiesDictionary()
: base(StringComparer.Ordinal)
{
}

IDictionary<string, object> IContextProperties.Properties => this;
}
}
4 changes: 2 additions & 2 deletions src/DurableTask.Core/TaskActivityDispatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ async Task OnProcessWorkItemAsync(TaskActivityWorkItem workItem)
ActivityExecutionResult? result;
try
{
await this.dispatchPipeline.RunAsync(dispatchContext, async _ =>
await this.dispatchPipeline.RunAsync(dispatchContext, async dispatchContext =>
{
if (taskActivity == null)
{
Expand All @@ -185,7 +185,7 @@ await this.dispatchPipeline.RunAsync(dispatchContext, async _ =>
throw new TypeMissingException($"TaskActivity {scheduledEvent.Name} version {scheduledEvent.Version} was not found");
}
var context = new TaskContext(taskMessage.OrchestrationInstance);
var context = new TaskContext(taskMessage.OrchestrationInstance, dispatchContext.Properties);
context.ErrorPropagationMode = this.errorPropagationMode;
HistoryEvent? responseEvent;
Expand Down
Loading

0 comments on commit dba54bd

Please sign in to comment.