diff --git a/Orleans.slnx b/Orleans.slnx
index ec2dd21f427..dd0bac06746 100644
--- a/Orleans.slnx
+++ b/Orleans.slnx
@@ -122,6 +122,7 @@
+
diff --git a/src/Dashboard/Orleans.Dashboard.App/package-lock.json b/src/Dashboard/Orleans.Dashboard.App/package-lock.json
index 5de02c58ce5..1428e6dd114 100644
--- a/src/Dashboard/Orleans.Dashboard.App/package-lock.json
+++ b/src/Dashboard/Orleans.Dashboard.App/package-lock.json
@@ -61,7 +61,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -1367,7 +1366,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz",
"integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==",
"dev": true,
- "peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -1407,7 +1405,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -1543,7 +1540,6 @@
"url": "https://github.com/sponsors/ai"
}
],
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751",
@@ -1607,7 +1603,6 @@
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
- "peer": true,
"dependencies": {
"@kurkle/color": "^0.3.0"
},
@@ -1772,7 +1767,6 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true,
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -2078,10 +2072,11 @@
}
},
"node_modules/flatted": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
- "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
- "dev": true
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "dev": true,
+ "license": "ISC"
},
"node_modules/fsevents": {
"version": "2.3.3",
@@ -2613,7 +2608,6 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
- "peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -2676,7 +2670,6 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
"integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==",
"dev": true,
- "peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -2890,7 +2883,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
- "peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
diff --git a/src/Orleans.BroadcastChannel/SubscriberTable/Predicates/DefaultStreamNamespacePredicateProvider.cs b/src/Orleans.BroadcastChannel/SubscriberTable/Predicates/DefaultStreamNamespacePredicateProvider.cs
index 9afd51e5088..bb1223310b4 100644
--- a/src/Orleans.BroadcastChannel/SubscriberTable/Predicates/DefaultStreamNamespacePredicateProvider.cs
+++ b/src/Orleans.BroadcastChannel/SubscriberTable/Predicates/DefaultStreamNamespacePredicateProvider.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using Orleans.Serialization.TypeSystem;
namespace Orleans.BroadcastChannel
@@ -7,7 +8,7 @@ namespace Orleans.BroadcastChannel
/// Default implementation of for internally supported stream predicates.
///
public class DefaultChannelNamespacePredicateProvider : IChannelNamespacePredicateProvider
- {
+ {
///
public bool TryGetPredicate(string predicatePattern, out IChannelNamespacePredicate predicate)
{
@@ -30,17 +31,38 @@ public bool TryGetPredicate(string predicatePattern, out IChannelNamespacePredic
}
///
- /// Stream namespace predicate provider which supports objects which can be constructed and optionally accept a string as a constructor argument.
+ /// Channel namespace predicate provider which supports objects which can be constructed and optionally accept a string as a constructor argument.
///
public class ConstructorChannelNamespacePredicateProvider : IChannelNamespacePredicateProvider
{
+#if NET9_0_OR_GREATER
+ private readonly Lock _lock = new();
+#else
+ private readonly object _lock = new();
+#endif
+ private readonly Dictionary _allowedPredicateTypes = new(StringComparer.Ordinal);
+
///
/// The prefix used to identify this predicate provider.
///
public const string Prefix = "ctor";
///
- /// Formats a stream namespace predicate which indicates a concrete type to be constructed, along with an optional argument.
+ /// Registers a predicate type as allowed for construction.
+ ///
+ /// The predicate type to register.
+ public void RegisterPredicateType(Type predicateType)
+ {
+ ArgumentNullException.ThrowIfNull(predicateType);
+ var typeName = RuntimeTypeNameFormatter.Format(predicateType);
+ lock (_lock)
+ {
+ _allowedPredicateTypes[typeName] = true;
+ }
+ }
+
+ ///
+ /// Formats a channel namespace predicate which indicates a concrete type to be constructed, along with an optional argument.
///
public static string FormatPattern(Type predicateType, string constructorArgument)
{
@@ -76,7 +98,24 @@ public bool TryGetPredicate(string predicatePattern, out IChannelNamespacePredic
arg = predicatePattern[(index + 1)..];
}
+ bool allowed;
+ lock (_lock)
+ {
+ allowed = _allowedPredicateTypes.ContainsKey(typeName);
+ }
+
+ if (!allowed)
+ {
+ throw new InvalidOperationException($"Type \"{typeName}\" is not a registered channel namespace predicate. Ensure the grain interface assembly is loaded and the predicate type is used in an [{nameof(ImplicitChannelSubscriptionAttribute)}].");
+ }
+
var type = Type.GetType(typeName, throwOnError: true);
+
+ if (!typeof(IChannelNamespacePredicate).IsAssignableFrom(type))
+ {
+ throw new InvalidOperationException($"Type \"{type}\" is not a valid channel namespace predicate because it does not implement {nameof(IChannelNamespacePredicate)}.");
+ }
+
if (string.IsNullOrEmpty(arg))
{
predicate = (IChannelNamespacePredicate)Activator.CreateInstance(type);
diff --git a/src/Orleans.BroadcastChannel/SubscriberTable/Predicates/ImplicitChannelSubscriptionAttribute.cs b/src/Orleans.BroadcastChannel/SubscriberTable/Predicates/ImplicitChannelSubscriptionAttribute.cs
index 0932743273a..3783dadb786 100644
--- a/src/Orleans.BroadcastChannel/SubscriberTable/Predicates/ImplicitChannelSubscriptionAttribute.cs
+++ b/src/Orleans.BroadcastChannel/SubscriberTable/Predicates/ImplicitChannelSubscriptionAttribute.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.DependencyInjection;
using Orleans.BroadcastChannel;
using Orleans.Metadata;
using Orleans.Runtime;
@@ -70,6 +71,16 @@ public ImplicitChannelSubscriptionAttribute(IChannelNamespacePredicate predicate
///
public IEnumerable> GetBindings(IServiceProvider services, Type grainClass, GrainType grainType)
{
+ // Register the predicate type so the constructor provider will accept it.
+ foreach (var provider in services.GetServices())
+ {
+ if (provider is ConstructorChannelNamespacePredicateProvider ctorProvider)
+ {
+ ctorProvider.RegisterPredicateType(Predicate.GetType());
+ break;
+ }
+ }
+
var binding = new Dictionary
{
[WellKnownGrainTypeProperties.BindingTypeKey] = WellKnownGrainTypeProperties.BroadcastChannelBindingTypeValue,
diff --git a/src/Orleans.Streaming/Predicates/StreamSubscriptionAttributes.cs b/src/Orleans.Streaming/Predicates/StreamSubscriptionAttributes.cs
index bd1b9845c90..acd010ab771 100644
--- a/src/Orleans.Streaming/Predicates/StreamSubscriptionAttributes.cs
+++ b/src/Orleans.Streaming/Predicates/StreamSubscriptionAttributes.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.DependencyInjection;
using Orleans.Metadata;
using Orleans.Runtime;
using Orleans.Streams;
@@ -73,6 +74,16 @@ public ImplicitStreamSubscriptionAttribute(IStreamNamespacePredicate predicate,
///
public IEnumerable> GetBindings(IServiceProvider services, Type grainClass, GrainType grainType)
{
+ // Register the predicate type so the constructor provider will accept it.
+ foreach (var provider in services.GetServices())
+ {
+ if (provider is ConstructorStreamNamespacePredicateProvider ctorProvider)
+ {
+ ctorProvider.RegisterPredicateType(Predicate.GetType());
+ break;
+ }
+ }
+
var binding = new Dictionary
{
[WellKnownGrainTypeProperties.BindingTypeKey] = WellKnownGrainTypeProperties.StreamBindingTypeValue,
diff --git a/src/Orleans.Streaming/PubSub/DefaultStreamNamespacePredicateProvider.cs b/src/Orleans.Streaming/PubSub/DefaultStreamNamespacePredicateProvider.cs
index 6ee3f479f04..e7b8ca4b97a 100644
--- a/src/Orleans.Streaming/PubSub/DefaultStreamNamespacePredicateProvider.cs
+++ b/src/Orleans.Streaming/PubSub/DefaultStreamNamespacePredicateProvider.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using Orleans.Serialization.TypeSystem;
namespace Orleans.Streams
@@ -7,7 +8,7 @@ namespace Orleans.Streams
/// Default implementation of for internally supported stream predicates.
///
public class DefaultStreamNamespacePredicateProvider : IStreamNamespacePredicateProvider
- {
+ {
///
public bool TryGetPredicate(string predicatePattern, out IStreamNamespacePredicate predicate)
{
@@ -34,11 +35,32 @@ public bool TryGetPredicate(string predicatePattern, out IStreamNamespacePredica
///
public class ConstructorStreamNamespacePredicateProvider : IStreamNamespacePredicateProvider
{
+#if NET9_0_OR_GREATER
+ private readonly Lock _lock = new();
+#else
+ private readonly object _lock = new();
+#endif
+ private readonly Dictionary _allowedPredicateTypes = new(StringComparer.Ordinal);
+
///
/// The prefix used to identify this predicate provider.
///
public const string Prefix = "ctor";
+ ///
+ /// Registers a predicate type as allowed for construction.
+ ///
+ /// The predicate type to register.
+ public void RegisterPredicateType(Type predicateType)
+ {
+ ArgumentNullException.ThrowIfNull(predicateType);
+ var typeName = RuntimeTypeNameFormatter.Format(predicateType);
+ lock (_lock)
+ {
+ _allowedPredicateTypes[typeName] = true;
+ }
+ }
+
///
/// Formats a stream namespace predicate which indicates a concrete type to be constructed, along with an optional argument.
///
@@ -76,7 +98,24 @@ public bool TryGetPredicate(string predicatePattern, out IStreamNamespacePredica
arg = predicatePattern[(index + 1)..];
}
+ bool allowed;
+ lock (_lock)
+ {
+ allowed = _allowedPredicateTypes.ContainsKey(typeName);
+ }
+
+ if (!allowed)
+ {
+ throw new InvalidOperationException($"Type \"{typeName}\" is not a registered stream namespace predicate. Ensure the grain interface assembly is loaded and the predicate type is used in an [{nameof(ImplicitStreamSubscriptionAttribute)}].");
+ }
+
var type = Type.GetType(typeName, throwOnError: true);
+
+ if (!typeof(IStreamNamespacePredicate).IsAssignableFrom(type))
+ {
+ throw new InvalidOperationException($"Type \"{type}\" is not a valid stream namespace predicate because it does not implement {nameof(IStreamNamespacePredicate)}.");
+ }
+
if (string.IsNullOrEmpty(arg))
{
predicate = (IStreamNamespacePredicate)Activator.CreateInstance(type);
diff --git a/test/Orleans.BroadcastChannel.Tests/ConstructorChannelNamespacePredicateProviderTests.cs b/test/Orleans.BroadcastChannel.Tests/ConstructorChannelNamespacePredicateProviderTests.cs
new file mode 100644
index 00000000000..df111f0c040
--- /dev/null
+++ b/test/Orleans.BroadcastChannel.Tests/ConstructorChannelNamespacePredicateProviderTests.cs
@@ -0,0 +1,89 @@
+using System;
+using Orleans.BroadcastChannel;
+using Orleans.Serialization.TypeSystem;
+using Xunit;
+
+namespace UnitTests;
+
+///
+/// Tests for predicate type registration and resolution.
+///
+[TestCategory("BVT"), TestCategory("Predicates")]
+public class ConstructorChannelNamespacePredicateProviderTests
+{
+ [Fact]
+ public void RegisteredPredicateType_Succeeds()
+ {
+ var provider = new ConstructorChannelNamespacePredicateProvider();
+ provider.RegisterPredicateType(typeof(TestChannelPredicate));
+ var pattern = ConstructorChannelNamespacePredicateProvider.FormatPattern(typeof(TestChannelPredicate), constructorArgument: null);
+
+ var result = provider.TryGetPredicate(pattern, out var predicate);
+
+ Assert.True(result);
+ Assert.NotNull(predicate);
+ Assert.IsType(predicate);
+ }
+
+ [Fact]
+ public void RegisteredPredicateTypeWithArg_Succeeds()
+ {
+ var provider = new ConstructorChannelNamespacePredicateProvider();
+ provider.RegisterPredicateType(typeof(TestChannelPredicateWithArg));
+ var pattern = ConstructorChannelNamespacePredicateProvider.FormatPattern(typeof(TestChannelPredicateWithArg), constructorArgument: "ch-ns");
+
+ var result = provider.TryGetPredicate(pattern, out var predicate);
+
+ Assert.True(result);
+ Assert.NotNull(predicate);
+ Assert.IsType(predicate);
+ Assert.True(predicate.IsMatch("ch-ns"));
+ }
+
+ [Fact]
+ public void UnregisteredType_Throws()
+ {
+ var provider = new ConstructorChannelNamespacePredicateProvider();
+ var pattern = $"ctor:{RuntimeTypeNameFormatter.Format(typeof(System.IO.FileInfo))}:C:\\temp\\evil.txt";
+
+ Assert.Throws(() => provider.TryGetPredicate(pattern, out _));
+ }
+
+ [Fact]
+ public void UnregisteredArbitraryType_Throws()
+ {
+ var provider = new ConstructorChannelNamespacePredicateProvider();
+ var pattern = $"ctor:{RuntimeTypeNameFormatter.Format(typeof(System.Collections.ArrayList))}";
+
+ Assert.Throws(() => provider.TryGetPredicate(pattern, out _));
+ }
+
+ [Fact]
+ public void NonMatchingPrefix_ReturnsFalse()
+ {
+ var provider = new ConstructorChannelNamespacePredicateProvider();
+
+ var result = provider.TryGetPredicate("namespace:test", out var predicate);
+
+ Assert.False(result);
+ Assert.Null(predicate);
+ }
+
+ public class TestChannelPredicate : IChannelNamespacePredicate
+ {
+ public string PredicatePattern => ConstructorChannelNamespacePredicateProvider.FormatPattern(typeof(TestChannelPredicate), constructorArgument: null);
+
+ public bool IsMatch(string streamNamespace) => true;
+ }
+
+ public class TestChannelPredicateWithArg : IChannelNamespacePredicate
+ {
+ private readonly string _namespace;
+
+ public TestChannelPredicateWithArg(string ns) => _namespace = ns;
+
+ public string PredicatePattern => ConstructorChannelNamespacePredicateProvider.FormatPattern(typeof(TestChannelPredicateWithArg), _namespace);
+
+ public bool IsMatch(string streamNamespace) => string.Equals(_namespace, streamNamespace, StringComparison.Ordinal);
+ }
+}
diff --git a/test/Orleans.BroadcastChannel.Tests/Orleans.BroadcastChannel.Tests.csproj b/test/Orleans.BroadcastChannel.Tests/Orleans.BroadcastChannel.Tests.csproj
new file mode 100644
index 00000000000..d815e2fa191
--- /dev/null
+++ b/test/Orleans.BroadcastChannel.Tests/Orleans.BroadcastChannel.Tests.csproj
@@ -0,0 +1,17 @@
+
+
+ UnitTests
+ $(TestTargetFrameworks)
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Orleans.Streaming.Tests/StreamingTests/ConstructorStreamNamespacePredicateProviderTests.cs b/test/Orleans.Streaming.Tests/StreamingTests/ConstructorStreamNamespacePredicateProviderTests.cs
new file mode 100644
index 00000000000..c117f534b4f
--- /dev/null
+++ b/test/Orleans.Streaming.Tests/StreamingTests/ConstructorStreamNamespacePredicateProviderTests.cs
@@ -0,0 +1,89 @@
+using System;
+using Orleans.Serialization.TypeSystem;
+using Orleans.Streams;
+using Xunit;
+
+namespace UnitTests;
+
+///
+/// Tests for predicate type registration and resolution.
+///
+[TestCategory("BVT"), TestCategory("Predicates")]
+public class ConstructorStreamNamespacePredicateProviderTests
+{
+ [Fact]
+ public void RegisteredPredicateType_Succeeds()
+ {
+ var provider = new ConstructorStreamNamespacePredicateProvider();
+ provider.RegisterPredicateType(typeof(TestStreamPredicate));
+ var pattern = ConstructorStreamNamespacePredicateProvider.FormatPattern(typeof(TestStreamPredicate), constructorArgument: null);
+
+ var result = provider.TryGetPredicate(pattern, out var predicate);
+
+ Assert.True(result);
+ Assert.NotNull(predicate);
+ Assert.IsType(predicate);
+ }
+
+ [Fact]
+ public void RegisteredPredicateTypeWithArg_Succeeds()
+ {
+ var provider = new ConstructorStreamNamespacePredicateProvider();
+ provider.RegisterPredicateType(typeof(TestStreamPredicateWithArg));
+ var pattern = ConstructorStreamNamespacePredicateProvider.FormatPattern(typeof(TestStreamPredicateWithArg), constructorArgument: "test-ns");
+
+ var result = provider.TryGetPredicate(pattern, out var predicate);
+
+ Assert.True(result);
+ Assert.NotNull(predicate);
+ Assert.IsType(predicate);
+ Assert.True(predicate.IsMatch("test-ns"));
+ }
+
+ [Fact]
+ public void UnregisteredType_Throws()
+ {
+ var provider = new ConstructorStreamNamespacePredicateProvider();
+ var pattern = $"ctor:{RuntimeTypeNameFormatter.Format(typeof(System.IO.FileInfo))}:C:\\temp\\evil.txt";
+
+ Assert.Throws(() => provider.TryGetPredicate(pattern, out _));
+ }
+
+ [Fact]
+ public void UnregisteredArbitraryType_Throws()
+ {
+ var provider = new ConstructorStreamNamespacePredicateProvider();
+ var pattern = $"ctor:{RuntimeTypeNameFormatter.Format(typeof(System.Collections.ArrayList))}";
+
+ Assert.Throws(() => provider.TryGetPredicate(pattern, out _));
+ }
+
+ [Fact]
+ public void NonMatchingPrefix_ReturnsFalse()
+ {
+ var provider = new ConstructorStreamNamespacePredicateProvider();
+
+ var result = provider.TryGetPredicate("namespace:test", out var predicate);
+
+ Assert.False(result);
+ Assert.Null(predicate);
+ }
+
+ public class TestStreamPredicate : IStreamNamespacePredicate
+ {
+ public string PredicatePattern => ConstructorStreamNamespacePredicateProvider.FormatPattern(typeof(TestStreamPredicate), constructorArgument: null);
+
+ public bool IsMatch(string streamNamespace) => true;
+ }
+
+ public class TestStreamPredicateWithArg : IStreamNamespacePredicate
+ {
+ private readonly string _namespace;
+
+ public TestStreamPredicateWithArg(string ns) => _namespace = ns;
+
+ public string PredicatePattern => ConstructorStreamNamespacePredicateProvider.FormatPattern(typeof(TestStreamPredicateWithArg), _namespace);
+
+ public bool IsMatch(string streamNamespace) => string.Equals(_namespace, streamNamespace, StringComparison.Ordinal);
+ }
+}