diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Validation/ActorNameRules.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Validation/ActorNameRules.cs
new file mode 100644
index 00000000..c946c184
--- /dev/null
+++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Validation/ActorNameRules.cs
@@ -0,0 +1,62 @@
+using System.Text;
+
+namespace ZB.MOM.WW.ScadaBridge.Commons.Validation;
+
+///
+/// Mirrors Akka.NET's ActorPath.IsValidPathElement (Commons does not reference Akka) so
+/// entity names that become actor names can be rejected at save time instead of crashing
+/// the runtime when the actor is created.
+///
+public static class ActorNameRules
+{
+ private const string AllowedSymbols = "-_.*$+:@&=,!~';";
+
+ /// True when is a legal actor path element.
+ public static bool IsValid(string? name)
+ {
+ if (string.IsNullOrEmpty(name) || name[0] == '$')
+ {
+ return false;
+ }
+
+ for (var i = 0; i < name.Length; i++)
+ {
+ var c = name[i];
+ if (IsAsciiLetterOrDigit(c) || AllowedSymbols.Contains(c))
+ {
+ continue;
+ }
+
+ // Akka permits percent-encoded octets (%HH).
+ if (c == '%' && i + 2 < name.Length && Uri.IsHexDigit(name[i + 1]) && Uri.IsHexDigit(name[i + 2]))
+ {
+ i += 2;
+ continue;
+ }
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /// A readable error for an invalid name, or null when the name is valid.
+ public static string? Describe(string? name)
+ {
+ if (IsValid(name))
+ {
+ return null;
+ }
+
+ if (string.IsNullOrEmpty(name))
+ {
+ return "Name must not be empty.";
+ }
+
+ return $"Name '{name}' contains characters not allowed in a runtime actor name " +
+ $"(allowed: letters, digits and {AllowedSymbols}); it must also not start with '$'.";
+ }
+
+ private static bool IsAsciiLetterOrDigit(char c) =>
+ (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9');
+}
diff --git a/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Validation/ActorNameRulesTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Validation/ActorNameRulesTests.cs
new file mode 100644
index 00000000..b6f6ffba
--- /dev/null
+++ b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Validation/ActorNameRulesTests.cs
@@ -0,0 +1,39 @@
+using ZB.MOM.WW.ScadaBridge.Commons.Validation;
+
+namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Validation;
+
+public class ActorNameRulesTests
+{
+ [Theory]
+ [InlineData("a b")]
+ [InlineData("Furnace #3")]
+ [InlineData("Ofen Süd")]
+ [InlineData("x/y")]
+ [InlineData("")]
+ [InlineData(null)]
+ [InlineData("$abc")]
+ [InlineData("100%")]
+ public void IsValid_RejectsIllegalNames(string? name)
+ {
+ Assert.False(ActorNameRules.IsValid(name));
+ Assert.NotNull(ActorNameRules.Describe(name));
+ }
+
+ [Theory]
+ [InlineData("Furnace-3")]
+ [InlineData("Line_1.A")]
+ [InlineData("a%20b")]
+ public void IsValid_AcceptsLegalNames(string name)
+ {
+ Assert.True(ActorNameRules.IsValid(name));
+ Assert.Null(ActorNameRules.Describe(name));
+ }
+
+ [Fact]
+ public void Describe_NamesTheOffendingValue()
+ {
+ var msg = ActorNameRules.Describe("Furnace #3");
+ Assert.Contains("'Furnace #3'", msg);
+ Assert.Contains("not allowed in a runtime actor name", msg);
+ }
+}