diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs
index 7b221e1435..93833dc108 100644
--- a/src/Umbraco.Core/Constants-Conventions.cs
+++ b/src/Umbraco.Core/Constants-Conventions.cs
@@ -348,5 +348,10 @@ public static partial class Constants
// TODO: return a list of built in types so we can use that to prevent deletion in the uI
}
+
+ public static class Udi
+ {
+ public const string Prefix = "umb://";
+ }
}
}
diff --git a/src/Umbraco.Core/GuidUdi.cs b/src/Umbraco.Core/GuidUdi.cs
index e8280bceb6..7f95299fd8 100644
--- a/src/Umbraco.Core/GuidUdi.cs
+++ b/src/Umbraco.Core/GuidUdi.cs
@@ -1,4 +1,5 @@
using System.ComponentModel;
+using Umbraco.Extensions;
namespace Umbraco.Cms.Core;
@@ -14,7 +15,7 @@ public class GuidUdi : Udi
/// The entity type part of the udi.
/// The guid part of the udi.
public GuidUdi(string entityType, Guid guid)
- : base(entityType, "umb://" + entityType + "/" + guid.ToString("N")) =>
+ : base(entityType, CreateStringValue(entityType, guid)) =>
Guid = guid;
///
@@ -57,4 +58,19 @@ public class GuidUdi : Udi
EnsureNotRoot();
return this;
}
+
+ private static string CreateStringValue(ReadOnlySpan entityType, Guid guid)
+ {
+ var startUdiLength = Constants.Conventions.Udi.Prefix.Length;
+ var outputSize = entityType.Length + startUdiLength + 32 + 1; //Based on the format umb://entityType/guid (32 = Guid N format, 1 = / between entityType and guid)
+ Span output = stackalloc char[outputSize];
+
+ //Add all the values of the format to the output
+ Constants.Conventions.Udi.Prefix.CopyTo(output[..startUdiLength]);
+ entityType.CopyTo(output.Slice(startUdiLength, entityType.Length));
+ output[startUdiLength + entityType.Length] = '/';
+ guid.TryFormat(output.Slice(outputSize - 32, 32), out _, "N");
+
+ return new string(output);
+ }
}
diff --git a/src/Umbraco.Core/StringUdi.cs b/src/Umbraco.Core/StringUdi.cs
index 2b1229be77..32e3677e2f 100644
--- a/src/Umbraco.Core/StringUdi.cs
+++ b/src/Umbraco.Core/StringUdi.cs
@@ -14,7 +14,7 @@ public class StringUdi : Udi
/// The entity type part of the udi.
/// The string id part of the udi.
public StringUdi(string entityType, string id)
- : base(entityType, "umb://" + entityType + "/" + EscapeUriString(id)) =>
+ : base(entityType, Constants.Conventions.Udi.Prefix + entityType + "/" + EscapeUriString(id)) =>
Id = id;
///
diff --git a/tests/Umbraco.Tests.Benchmarks/GuidUdiBenchmarks.cs b/tests/Umbraco.Tests.Benchmarks/GuidUdiBenchmarks.cs
new file mode 100644
index 0000000000..f9946aba75
--- /dev/null
+++ b/tests/Umbraco.Tests.Benchmarks/GuidUdiBenchmarks.cs
@@ -0,0 +1,88 @@
+using System;
+using BenchmarkDotNet.Attributes;
+using Umbraco.Cms.Core;
+using Umbraco.Tests.Benchmarks.Config;
+
+namespace Umbraco.Tests.Benchmarks
+{
+ [QuickRunWithMemoryDiagnoserConfig]
+ public class GuidUdiBenchmarks
+ {
+ private readonly Guid _guid = Guid.NewGuid();
+ private readonly string _entityType = Constants.UdiEntityType.DocumentType;
+
+ [Benchmark(Baseline = true)]
+ public Udi CurrentInit()
+ {
+ return new OldGuidUdi(_entityType, _guid);
+ }
+
+ [Benchmark()]
+ public Udi StringPolationInit()
+ {
+ return new StringPolationGuidUdi(_entityType, _guid);
+ }
+
+ [Benchmark]
+ public Udi NewInit()
+ {
+ return new NewGuidUdi(_entityType, _guid);
+ }
+
+ public class OldGuidUdi : Udi
+ {
+ public Guid Guid { get; }
+
+ public override bool IsRoot => throw new NotImplementedException();
+
+ public OldGuidUdi(string entityType, Guid guid)
+ : base(entityType, "umb://" + entityType + "/" + guid.ToString("N")) =>
+ Guid = guid;
+ }
+
+ public class StringPolationGuidUdi : Udi
+ {
+ public Guid Guid { get; }
+
+ public override bool IsRoot => throw new NotImplementedException();
+
+ public StringPolationGuidUdi(string entityType, Guid guid)
+ : base(entityType, $"umb://{entityType}/{guid:N}") =>
+ Guid = guid;
+ }
+
+ public class NewGuidUdi : Udi
+ {
+ public Guid Guid { get; }
+
+ public override bool IsRoot => throw new NotImplementedException();
+
+ public NewGuidUdi(string entityType, Guid guid) : base(entityType, GetStringValue(entityType, guid))
+ {
+ Guid = guid;
+ }
+
+ public static string GetStringValue(ReadOnlySpan entityType, Guid guid)
+ {
+ var startUdiLength = Constants.Conventions.Udi.Prefix.Length;
+ var outputSize = entityType.Length + startUdiLength + 32 + 1;
+ Span output = stackalloc char[outputSize];
+
+ Constants.Conventions.Udi.Prefix.CopyTo(output[..startUdiLength]);
+ entityType.CopyTo(output.Slice(startUdiLength, entityType.Length));
+ output[startUdiLength + entityType.Length] = '/';
+ guid.TryFormat(output.Slice(outputSize - 32, 32), out _, "N");
+
+ return new string(output);
+ }
+ }
+
+ // I think we are currently bottlenecked by the 'new Udi(string)' not accepting a span. If it did, then we wouldn't have to create a string mid creation and we would be able to cut back on the allocated data
+ //
+ //| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Allocated |
+ //|------------------- |---------:|----------:|---------:|------:|--------:|-------:|----------:|
+ //| CurrentInit | 562.2 ns | 242.84 ns | 13.31 ns | 1.00 | 0.00 | 0.1113 | 352 B |
+ //| StringPolationInit | 589.3 ns | 530.08 ns | 29.06 ns | 1.05 | 0.07 | 0.0811 | 264 B |
+ //| NewInit | 533.6 ns | 40.55 ns | 2.22 ns | 0.95 | 0.03 | 0.0808 | 264 B |
+ }
+}