diff --git a/src/Umbraco.Core/ByteArrayExtensions.cs b/src/Umbraco.Core/ByteArrayExtensions.cs
deleted file mode 100644
index dacdd509ca..0000000000
--- a/src/Umbraco.Core/ByteArrayExtensions.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-namespace Umbraco.Core
-{
- public static class ByteArrayExtensions
- {
- private static readonly char[] BytesToHexStringLookup = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
-
- public static string ToHexString(this byte[] bytes)
- {
- int i = 0, p = 0, bytesLength = bytes.Length;
- var chars = new char[bytesLength * 2];
- while (i < bytesLength)
- {
- var b = bytes[i++];
- chars[p++] = BytesToHexStringLookup[b / 0x10];
- chars[p++] = BytesToHexStringLookup[b % 0x10];
- }
- return new string(chars, 0, chars.Length);
- }
-
- public static string ToHexString(this byte[] bytes, char separator, int blockSize, int blockCount)
- {
- int p = 0, bytesLength = bytes.Length, count = 0, size = 0;
- var chars = new char[bytesLength * 2 + blockCount];
- for (var i = 0; i < bytesLength; i++)
- {
- var b = bytes[i++];
- chars[p++] = BytesToHexStringLookup[b / 0x10];
- chars[p++] = BytesToHexStringLookup[b % 0x10];
- if (count == blockCount) continue;
- if (++size < blockSize) continue;
-
- chars[p++] = '/';
- size = 0;
- count++;
- }
- return new string(chars, 0, chars.Length);
- }
- }
-}
diff --git a/src/Umbraco.Core/GuidUtils.cs b/src/Umbraco.Core/GuidUtils.cs
new file mode 100644
index 0000000000..3768e1385d
--- /dev/null
+++ b/src/Umbraco.Core/GuidUtils.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Umbraco.Core
+{
+ ///
+ /// Utility methods for the struct.
+ ///
+ internal static class GuidUtils
+ {
+ ///
+ /// Combines two guid instances utilizing an exclusive disjunction.
+ /// The resultant guid is not guaranteed to be unique since the number of unique bits is halved.
+ ///
+ /// The first guid.
+ /// The seconds guid.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static Guid Combine(Guid a, Guid b)
+ {
+ var ad = new DecomposedGuid(a);
+ var bd = new DecomposedGuid(b);
+
+ ad.Hi ^= bd.Hi;
+ ad.Lo ^= bd.Lo;
+
+ return ad.Value;
+ }
+
+ ///
+ /// A decomposed guid. Allows access to the high and low bits without unsafe code.
+ ///
+ [StructLayout(LayoutKind.Explicit)]
+ private struct DecomposedGuid
+ {
+ [FieldOffset(00)] public Guid Value;
+ [FieldOffset(00)] public long Hi;
+ [FieldOffset(08)] public long Lo;
+
+ public DecomposedGuid(Guid value) : this() => this.Value = value;
+ }
+ }
+}
diff --git a/src/Umbraco.Core/HexEncoder.cs b/src/Umbraco.Core/HexEncoder.cs
new file mode 100644
index 0000000000..073dc8b543
--- /dev/null
+++ b/src/Umbraco.Core/HexEncoder.cs
@@ -0,0 +1,84 @@
+using System.Linq;
+using System.Runtime.CompilerServices;
+
+namespace Umbraco.Core
+{
+ ///
+ /// Provides methods for encoding byte arrays into hexidecimal strings.
+ ///
+ internal static class HexEncoder
+ {
+ // LUT's that provide the hexidecimal representation of each possible byte value.
+ private static readonly char[] HexLutBase = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
+
+ // The base LUT arranged in 16x each item order. 0 * 16, 1 * 16, .... F * 16
+ private static readonly char[] HexLutHi = Enumerable.Range(0, 256).Select(x => HexLutBase[x / 0x10]).ToArray();
+
+ // The base LUT repeated 16x.
+ private static readonly char[] HexLutLo = Enumerable.Range(0, 256).Select(x => HexLutBase[x % 0x10]).ToArray();
+
+ ///
+ /// Converts a to a hexidecimal formatted padded to 2 digits.
+ ///
+ /// The bytes.
+ /// The .
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static string Encode(byte[] bytes)
+ {
+ var length = bytes.Length;
+ var chars = new char[length * 2];
+
+ var index = 0;
+ for (var i = 0; i < length; i++)
+ {
+ var byteIndex = bytes[i];
+ chars[index++] = HexLutHi[byteIndex];
+ chars[index++] = HexLutLo[byteIndex];
+ }
+
+ return new string(chars, 0, chars.Length);
+ }
+
+ ///
+ /// Converts a to a hexidecimal formatted padded to 2 digits
+ /// and split into blocks with the given char separator.
+ ///
+ /// The bytes.
+ /// The separator.
+ /// The block size.
+ /// The block count.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static string Encode(byte[] bytes, char separator, int blockSize, int blockCount)
+ {
+ var length = bytes.Length;
+ var chars = new char[(length * 2) + blockCount];
+ var count = 0;
+ var size = 0;
+ var index = 0;
+
+ for (var i = 0; i < length; i++)
+ {
+ var byteIndex = bytes[i];
+ chars[index++] = HexLutHi[byteIndex];
+ chars[index++] = HexLutLo[byteIndex];
+
+ if (count == blockCount)
+ {
+ continue;
+ }
+
+ if (++size < blockSize)
+ {
+ continue;
+ }
+
+ chars[index++] = separator;
+ size = 0;
+ count++;
+ }
+
+ return new string(chars, 0, chars.Length);
+ }
+ }
+}
diff --git a/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs b/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs
index ef71aff3bc..37c6a7b209 100644
--- a/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs
+++ b/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs
@@ -20,24 +20,11 @@ namespace Umbraco.Core.IO.MediaPathSchemes
{
// assumes that cuid and puid keys can be trusted - and that a single property type
// for a single content cannot store two different files with the same name
- var directory = Combine(itemGuid, propertyGuid).ToHexString(/*'/', 2, 4*/); // could use ext to fragment path eg 12/e4/f2/...
+ var directory = HexEncoder.Encode(GuidUtils.Combine(itemGuid, propertyGuid).ToByteArray()/*'/', 2, 4*/); // could use ext to fragment path eg 12/e4/f2/...
return Path.Combine(directory, filename).Replace('\\', '/');
}
///
- public string GetDeleteDirectory(string filepath)
- {
- return Path.GetDirectoryName(filepath);
- }
-
- private static byte[] Combine(Guid guid1, Guid guid2)
- {
- var bytes1 = guid1.ToByteArray();
- var bytes2 = guid2.ToByteArray();
- var bytes = new byte[bytes1.Length];
- for (var i = 0; i < bytes1.Length; i++)
- bytes[i] = (byte) (bytes1[i] ^ bytes2[i]);
- return bytes;
- }
+ public string GetDeleteDirectory(string filepath) => Path.GetDirectoryName(filepath);
}
}
diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs
index caa63d7526..88b1179f6d 100644
--- a/src/Umbraco.Core/Models/ContentTypeBase.cs
+++ b/src/Umbraco.Core/Models/ContentTypeBase.cs
@@ -92,8 +92,8 @@ namespace Umbraco.Core.Models
public readonly PropertyInfo AllowedAsRootSelector = ExpressionHelper.GetPropertyInfo(x => x.AllowedAsRoot);
public readonly PropertyInfo IsContainerSelector = ExpressionHelper.GetPropertyInfo(x => x.IsContainer);
public readonly PropertyInfo AllowedContentTypesSelector = ExpressionHelper.GetPropertyInfo>(x => x.AllowedContentTypes);
- public readonly PropertyInfo PropertyGroupCollectionSelector = ExpressionHelper.GetPropertyInfo(x => x.PropertyGroups);
- public readonly PropertyInfo PropertyTypeCollectionSelector = ExpressionHelper.GetPropertyInfo>(x => x.PropertyTypes);
+ public readonly PropertyInfo PropertyGroupsSelector = ExpressionHelper.GetPropertyInfo(x => x.PropertyGroups);
+ public readonly PropertyInfo PropertyTypesSelector = ExpressionHelper.GetPropertyInfo>(x => x.PropertyTypes);
public readonly PropertyInfo HasPropertyTypeBeenRemovedSelector = ExpressionHelper.GetPropertyInfo(x => x.HasPropertyTypeBeenRemoved);
public readonly PropertyInfo VaryBy = ExpressionHelper.GetPropertyInfo(x => x.Variations);
@@ -106,12 +106,12 @@ namespace Umbraco.Core.Models
protected void PropertyGroupsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
- OnPropertyChanged(Ps.Value.PropertyGroupCollectionSelector);
+ OnPropertyChanged(Ps.Value.PropertyGroupsSelector);
}
protected void PropertyTypesChanged(object sender, NotifyCollectionChangedEventArgs e)
{
- OnPropertyChanged(Ps.Value.PropertyTypeCollectionSelector);
+ OnPropertyChanged(Ps.Value.PropertyTypesSelector);
}
///
@@ -263,6 +263,8 @@ namespace Umbraco.Core.Models
get => _noGroupPropertyTypes;
set
{
+ if (_noGroupPropertyTypes != null)
+ _noGroupPropertyTypes.CollectionChanged -= PropertyTypesChanged;
_noGroupPropertyTypes = new PropertyTypeCollection(IsPublishing, value);
_noGroupPropertyTypes.CollectionChanged += PropertyTypesChanged;
PropertyTypesChanged(_noGroupPropertyTypes, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
@@ -376,7 +378,7 @@ namespace Umbraco.Core.Models
if (!HasPropertyTypeBeenRemoved)
{
HasPropertyTypeBeenRemoved = true;
- OnPropertyChanged(Ps.Value.PropertyTypeCollectionSelector);
+ OnPropertyChanged(Ps.Value.PropertyTypesSelector);
}
break;
}
@@ -388,7 +390,7 @@ namespace Umbraco.Core.Models
if (!HasPropertyTypeBeenRemoved)
{
HasPropertyTypeBeenRemoved = true;
- OnPropertyChanged(Ps.Value.PropertyTypeCollectionSelector);
+ OnPropertyChanged(Ps.Value.PropertyTypesSelector);
}
}
}
@@ -412,7 +414,7 @@ namespace Umbraco.Core.Models
// actually remove the group
PropertyGroups.RemoveItem(propertyGroupName);
- OnPropertyChanged(Ps.Value.PropertyGroupCollectionSelector);
+ OnPropertyChanged(Ps.Value.PropertyGroupsSelector);
}
///
diff --git a/src/Umbraco.Core/Models/PropertyGroup.cs b/src/Umbraco.Core/Models/PropertyGroup.cs
index 1d0b949932..595e8d1d6a 100644
--- a/src/Umbraco.Core/Models/PropertyGroup.cs
+++ b/src/Umbraco.Core/Models/PropertyGroup.cs
@@ -35,12 +35,12 @@ namespace Umbraco.Core.Models
{
public readonly PropertyInfo NameSelector = ExpressionHelper.GetPropertyInfo(x => x.Name);
public readonly PropertyInfo SortOrderSelector = ExpressionHelper.GetPropertyInfo(x => x.SortOrder);
- public readonly PropertyInfo PropertyTypeCollectionSelector = ExpressionHelper.GetPropertyInfo(x => x.PropertyTypes);
+ public readonly PropertyInfo PropertyTypes = ExpressionHelper.GetPropertyInfo(x => x.PropertyTypes);
}
private void PropertyTypesChanged(object sender, NotifyCollectionChangedEventArgs e)
{
- OnPropertyChanged(Ps.Value.PropertyTypeCollectionSelector);
+ OnPropertyChanged(Ps.Value.PropertyTypes);
}
///
@@ -76,6 +76,8 @@ namespace Umbraco.Core.Models
get => _propertyTypes;
set
{
+ if (_propertyTypes != null)
+ _propertyTypes.CollectionChanged -= PropertyTypesChanged;
_propertyTypes = value;
// since we're adding this collection to this group,
@@ -83,6 +85,7 @@ namespace Umbraco.Core.Models
foreach (var propertyType in _propertyTypes)
propertyType.PropertyGroupId = new Lazy(() => Id);
+ OnPropertyChanged(Ps.Value.PropertyTypes);
_propertyTypes.CollectionChanged += PropertyTypesChanged;
}
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
index 6be07a4c3d..44215b7f7e 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
@@ -190,8 +190,8 @@ AND umbracoNode.nodeObjectType = @objectType",
SortOrder = allowedContentType.SortOrder
});
}
-
-
+
+
//Insert Tabs
foreach (var propertyGroup in entity.PropertyGroups)
{
@@ -328,14 +328,14 @@ AND umbracoNode.id <> @id",
// We check if the entity's own PropertyTypes has been modified and then also check
// any of the property groups PropertyTypes has been modified.
// This specifically tells us if any property type collections have changed.
- if (entity.IsPropertyDirty("PropertyTypes") || entity.PropertyTypes.Any(pt => pt.IsDirty()))
+ if (entity.IsPropertyDirty("NoGroupPropertyTypes") || entity.PropertyGroups.Any(x => x.IsPropertyDirty("PropertyTypes")))
{
var dbPropertyTypes = Database.Fetch("WHERE contentTypeId = @Id", new { entity.Id });
- var dbPropertyTypeAlias = dbPropertyTypes.Select(x => x.Id);
+ var dbPropertyTypeIds = dbPropertyTypes.Select(x => x.Id);
var entityPropertyTypes = entity.PropertyTypes.Where(x => x.HasIdentity).Select(x => x.Id);
- var items = dbPropertyTypeAlias.Except(entityPropertyTypes);
- foreach (var item in items)
- DeletePropertyType(entity.Id, item);
+ var propertyTypeToDeleteIds = dbPropertyTypeIds.Except(entityPropertyTypes);
+ foreach (var propertyTypeId in propertyTypeToDeleteIds)
+ DeletePropertyType(entity.Id, propertyTypeId);
}
// Delete tabs ... by excepting entries from db with entries from collections.
@@ -620,7 +620,7 @@ AND umbracoNode.id <> @id",
var sqlDelete = Sql()
.Delete()
.WhereIn((System.Linq.Expressions.Expression>)(x => x.ContentKey), sqlSelect);
-
+
Database.Execute(sqlDelete);
}
@@ -690,7 +690,7 @@ AND umbracoNode.id <> @id",
//first clear out any existing names that might already exists under the default lang
//there's 2x tables to update
- //clear out the versionCultureVariation table
+ //clear out the versionCultureVariation table
var sqlSelect = Sql().Select(x => x.Id)
.From()
.InnerJoin().On(x => x.Id, x => x.VersionId)
diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj
index 3613e1bb87..1592292cca 100755
--- a/src/Umbraco.Core/Umbraco.Core.csproj
+++ b/src/Umbraco.Core/Umbraco.Core.csproj
@@ -114,7 +114,6 @@
-
@@ -324,6 +323,8 @@
+
+
diff --git a/src/Umbraco.Tests.Benchmarks/CombineGuidBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/CombineGuidBenchmarks.cs
new file mode 100644
index 0000000000..ce55f6890d
--- /dev/null
+++ b/src/Umbraco.Tests.Benchmarks/CombineGuidBenchmarks.cs
@@ -0,0 +1,48 @@
+using System;
+using BenchmarkDotNet.Attributes;
+using Umbraco.Core;
+using Umbraco.Tests.Benchmarks.Config;
+
+namespace Umbraco.Tests.Benchmarks
+{
+ [QuickRunWithMemoryDiagnoserConfig]
+ public class CombineGuidBenchmarks
+ {
+ private static readonly Guid _a = Guid.NewGuid();
+ private static readonly Guid _b = Guid.NewGuid();
+
+ [Benchmark]
+ public byte[] CombineUtils() => GuidUtils.Combine(_a, _b).ToByteArray();
+
+ [Benchmark]
+ public byte[] CombineLoop() => Combine(_a, _b);
+
+ private static byte[] Combine(Guid guid1, Guid guid2)
+ {
+ var bytes1 = guid1.ToByteArray();
+ var bytes2 = guid2.ToByteArray();
+ var bytes = new byte[bytes1.Length];
+ for (var i = 0; i < bytes1.Length; i++)
+ {
+ bytes[i] = (byte)(bytes1[i] ^ bytes2[i]);
+ }
+
+ return bytes;
+ }
+ }
+
+ // Nov 8 2018
+ //BenchmarkDotNet=v0.11.2, OS=Windows 10.0.17763.55 (1809/October2018Update/Redstone5)
+ //Intel Core i7-6600U CPU 2.60GHz(Skylake), 1 CPU, 4 logical and 2 physical cores
+ // [Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.3190.0
+ // Job-JIATTD : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.3190.0
+
+ //IterationCount=3 IterationTime=100.0000 ms LaunchCount = 1
+ //WarmupCount=3
+
+ // Method | Mean | Error | StdDev | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op |
+ //------------- |---------:|----------:|----------:|------------:|------------:|------------:|--------------------:|
+ // CombineUtils | 33.34 ns | 8.086 ns | 0.4432 ns | 0.0133 | - | - | 28 B |
+ // CombineLoop | 55.03 ns | 11.311 ns | 0.6200 ns | 0.0395 | - | - | 84 B |
+}
+
diff --git a/src/Umbraco.Tests.Benchmarks/HexStringBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/HexStringBenchmarks.cs
new file mode 100644
index 0000000000..e29a5a24f3
--- /dev/null
+++ b/src/Umbraco.Tests.Benchmarks/HexStringBenchmarks.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Text;
+using BenchmarkDotNet.Attributes;
+using Umbraco.Core;
+using Umbraco.Tests.Benchmarks.Config;
+
+namespace Umbraco.Tests.Benchmarks
+{
+ [QuickRunConfig]
+ public class HexStringBenchmarks
+ {
+ private byte[] _buffer;
+
+ [Params(8, 16, 32, 64, 128, 256)]
+ public int Count { get; set; }
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ this._buffer = new byte[this.Count];
+ var random = new Random();
+ random.NextBytes(this._buffer);
+ }
+
+ [Benchmark(Baseline = true)]
+ public string ToHexStringBuilder()
+ {
+ var sb = new StringBuilder(this._buffer.Length * 2);
+ for (var i = 0; i < this._buffer.Length; i++)
+ {
+ sb.Append(this._buffer[i].ToString("X2"));
+ }
+
+ return sb.ToString();
+ }
+
+ [Benchmark]
+ public string ToHexStringEncoder() => HexEncoder.Encode(this._buffer);
+ }
+
+ // Nov 8 2018
+ //BenchmarkDotNet=v0.11.2, OS=Windows 10.0.17763.55 (1809/October2018Update/Redstone5)
+ //Intel Core i7-6600U CPU 2.60GHz(Skylake), 1 CPU, 4 logical and 2 physical cores
+ // [Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.3190.0
+ // Job-JIATTD : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.3190.0
+
+ //IterationCount=3 IterationTime=100.0000 ms LaunchCount = 1
+ //WarmupCount=3
+
+ // Method | Count | Mean | Error | StdDev | Ratio |
+ //------------------- |------ |-------------:|-------------:|-----------:|------:|
+ // ToHexStringBuilder | 8 | 786.49 ns | 319.92 ns | 17.536 ns | 1.00 |
+ // ToHexStringEncoder | 8 | 64.19 ns | 30.21 ns | 1.656 ns | 0.08 |
+ // | | | | | |
+ // ToHexStringBuilder | 16 | 1,442.43 ns | 503.00 ns | 27.571 ns | 1.00 |
+ // ToHexStringEncoder | 16 | 133.46 ns | 177.55 ns | 9.732 ns | 0.09 |
+ // | | | | | |
+ // ToHexStringBuilder | 32 | 2,869.23 ns | 924.35 ns | 50.667 ns | 1.00 |
+ // ToHexStringEncoder | 32 | 181.03 ns | 96.64 ns | 5.297 ns | 0.06 |
+ // | | | | | |
+ // ToHexStringBuilder | 64 | 5,775.33 ns | 2,825.42 ns | 154.871 ns | 1.00 |
+ // ToHexStringEncoder | 64 | 331.16 ns | 125.63 ns | 6.886 ns | 0.06 |
+ // | | | | | |
+ // ToHexStringBuilder | 128 | 11,662.35 ns | 4,908.03 ns | 269.026 ns | 1.00 |
+ // ToHexStringEncoder | 128 | 633.78 ns | 57.56 ns | 3.155 ns | 0.05 |
+ // | | | | | |
+ // ToHexStringBuilder | 256 | 22,960.11 ns | 14,111.47 ns | 773.497 ns | 1.00 |
+ // ToHexStringEncoder | 256 | 1,224.76 ns | 547.27 ns | 29.998 ns | 0.05 |
+}
diff --git a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj
index 15a55ab6ac..bb14fb5a77 100644
--- a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj
+++ b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj
@@ -46,10 +46,12 @@
+
+
diff --git a/src/Umbraco.Tests/CoreThings/GuidUtilsTests.cs b/src/Umbraco.Tests/CoreThings/GuidUtilsTests.cs
new file mode 100644
index 0000000000..5ef8cba356
--- /dev/null
+++ b/src/Umbraco.Tests/CoreThings/GuidUtilsTests.cs
@@ -0,0 +1,32 @@
+using System;
+using NUnit.Framework;
+using Umbraco.Core;
+
+namespace Umbraco.Tests.CoreThings
+{
+ public class GuidUtilsTests
+ {
+ [Test]
+ public void GuidCombineMethodsAreEqual()
+ {
+ var a = Guid.NewGuid();
+ var b = Guid.NewGuid();
+
+ Assert.AreEqual(GuidUtils.Combine(a, b).ToByteArray(), Combine(a, b));
+ }
+
+ // Reference implementation taken from original code.
+ private static byte[] Combine(Guid guid1, Guid guid2)
+ {
+ var bytes1 = guid1.ToByteArray();
+ var bytes2 = guid2.ToByteArray();
+ var bytes = new byte[bytes1.Length];
+ for (var i = 0; i < bytes1.Length; i++)
+ {
+ bytes[i] = (byte)(bytes1[i] ^ bytes2[i]);
+ }
+
+ return bytes;
+ }
+ }
+}
diff --git a/src/Umbraco.Tests/CoreThings/HexEncoderTests.cs b/src/Umbraco.Tests/CoreThings/HexEncoderTests.cs
new file mode 100644
index 0000000000..588fff83e8
--- /dev/null
+++ b/src/Umbraco.Tests/CoreThings/HexEncoderTests.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Text;
+using NUnit.Framework;
+using Umbraco.Core;
+
+namespace Umbraco.Tests.CoreThings
+{
+ public class HexEncoderTests
+ {
+ [Test]
+ public void ToHexStringCreatesCorrectValue()
+ {
+ var buffer = new byte[255];
+ var random = new Random();
+ random.NextBytes(buffer);
+
+ var sb = new StringBuilder(buffer.Length * 2);
+ for (var i = 0; i < buffer.Length; i++)
+ {
+ sb.Append(buffer[i].ToString("X2"));
+ }
+
+ var expected = sb.ToString();
+
+ var actual = HexEncoder.Encode(buffer);
+ Assert.AreEqual(expected, actual);
+ }
+
+ [Test]
+ public void ToHexStringWithSeparatorCreatesCorrectValue()
+ {
+ var buffer = new byte[255];
+ var random = new Random();
+ random.NextBytes(buffer);
+
+ var expected = ToHexString(buffer, '/', 2, 4);
+ var actual = HexEncoder.Encode(buffer, '/', 2, 4);
+
+ Assert.AreEqual(expected, actual);
+ }
+
+ private static readonly char[] _bytesToHexStringLookup = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
+
+ // Reference implementation taken from original extension method.
+ private static string ToHexString(byte[] bytes, char separator, int blockSize, int blockCount)
+ {
+ int p = 0, bytesLength = bytes.Length, count = 0, size = 0;
+ var chars = new char[(bytesLength * 2) + blockCount];
+ for (var i = 0; i < bytesLength; i++)
+ {
+ var b = bytes[i];
+ chars[p++] = _bytesToHexStringLookup[b / 0x10];
+ chars[p++] = _bytesToHexStringLookup[b % 0x10];
+ if (count == blockCount)
+ {
+ continue;
+ }
+
+ if (++size < blockSize)
+ {
+ continue;
+ }
+
+ chars[p++] = separator;
+ size = 0;
+ count++;
+ }
+ return new string(chars, 0, chars.Length);
+ }
+ }
+}
diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj
index a9528ea5d8..b98e2ccf67 100644
--- a/src/Umbraco.Tests/Umbraco.Tests.csproj
+++ b/src/Umbraco.Tests/Umbraco.Tests.csproj
@@ -118,6 +118,8 @@
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/relationtype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/relationtype.resource.js
index f17aae0a6b..7f13a46d2f 100644
--- a/src/Umbraco.Web.UI.Client/src/common/resources/relationtype.resource.js
+++ b/src/Umbraco.Web.UI.Client/src/common/resources/relationtype.resource.js
@@ -35,7 +35,7 @@ function relationTypeResource($q, $http, umbRequestHelper, umbDataFormatter) {
/**
* @ngdoc method
* @name umbraco.resources.relationTypeResource#getRelationObjectTypes
- * @methodof umbraco.resources.relationTypeResource
+ * @methodOf umbraco.resources.relationTypeResource
*
* @description
* Gets a list of Umbraco object types which can be associated with a relation.
@@ -54,7 +54,7 @@ function relationTypeResource($q, $http, umbRequestHelper, umbDataFormatter) {
/**
* @ngdoc method
* @name umbraco.resources.relationTypeResource#save
- * @methodof umbraco.resources.relationTypeResource
+ * @methodOf umbraco.resources.relationTypeResource
*
* @description
* Updates a relation type.
@@ -74,7 +74,7 @@ function relationTypeResource($q, $http, umbRequestHelper, umbDataFormatter) {
/**
* @ngdoc method
* @name umbraco.resources.relationTypeResource#create
- * @methodof umbraco.resources.relationTypeResource
+ * @methodOf umbraco.resources.relationTypeResource
*
* @description
* Creates a new relation type.
@@ -94,7 +94,7 @@ function relationTypeResource($q, $http, umbRequestHelper, umbDataFormatter) {
/**
* @ngdoc method
* @name umbraco.resources.relationTypeResource#deleteById
- * @methodof umbraco.resources.relationTypeResource
+ * @methodOf umbraco.resources.relationTypeResource
*
* @description
* Deletes a relation type with a given ID.
diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj
index 1c64267c4c..115aac3027 100644
--- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj
+++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj
@@ -453,10 +453,10 @@
$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v11.0\Web\Microsoft.Web.Publishing.Tasks.dll
$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v12.0\Web\Microsoft.Web.Publishing.Tasks.dll
$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v14.0\Web\Microsoft.Web.Publishing.Tasks.dll
- $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v15.0\Web\Microsoft.Web.Publishing.Tasks.dll
- $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v16.0\Web\Microsoft.Web.Publishing.Tasks.dll
-
- $(ProgramFiles32)\Microsoft Visual Studio\2019\Preview\MSBuild\Microsoft\VisualStudio\v16.0\Web\Microsoft.Web.Publishing.Tasks.dll
+ $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v15.0\Web\Microsoft.Web.Publishing.Tasks.dll
+ $(MSBuildExtensionsPath)\Microsoft\VisualStudio\v16.0\Web\Microsoft.Web.Publishing.Tasks.dll
+
+ $(ProgramFiles32)\Microsoft Visual Studio\2019\Preview\MSBuild\Microsoft\VisualStudio\v16.0\Web\Microsoft.Web.Publishing.Tasks.dll
diff --git a/src/Umbraco.Web/Editors/DashboardController.cs b/src/Umbraco.Web/Editors/DashboardController.cs
index 72b7acc9e7..d8cbc34938 100644
--- a/src/Umbraco.Web/Editors/DashboardController.cs
+++ b/src/Umbraco.Web/Editors/DashboardController.cs
@@ -118,6 +118,7 @@ namespace Umbraco.Web.Editors
}
[ValidateAngularAntiForgeryToken]
+ [OutgoingEditorModelEvent]
public IEnumerable> GetDashboard(string section)
{
return _dashboards.GetDashboards(section, Security.CurrentUser);
diff --git a/src/Umbraco.Web/Editors/EditorModelEventArgs.cs b/src/Umbraco.Web/Editors/EditorModelEventArgs.cs
index 153a2d8786..daf262fce5 100644
--- a/src/Umbraco.Web/Editors/EditorModelEventArgs.cs
+++ b/src/Umbraco.Web/Editors/EditorModelEventArgs.cs
@@ -4,9 +4,13 @@ namespace Umbraco.Web.Editors
{
public sealed class EditorModelEventArgs : EditorModelEventArgs
{
+ private readonly EditorModelEventArgs _baseArgs;
+ private T _model;
+
public EditorModelEventArgs(EditorModelEventArgs baseArgs)
: base(baseArgs.Model, baseArgs.UmbracoContext)
{
+ _baseArgs = baseArgs;
Model = (T)baseArgs.Model;
}
@@ -16,7 +20,16 @@ namespace Umbraco.Web.Editors
Model = model;
}
- public new T Model { get; private set; }
+ public new T Model
+ {
+ get => _model;
+ set
+ {
+ _model = value;
+ if (_baseArgs != null)
+ _baseArgs.Model = _model;
+ }
+ }
}
public class EditorModelEventArgs : EventArgs
@@ -27,7 +40,7 @@ namespace Umbraco.Web.Editors
UmbracoContext = umbracoContext;
}
- public object Model { get; private set; }
- public UmbracoContext UmbracoContext { get; private set; }
+ public object Model { get; set; }
+ public UmbracoContext UmbracoContext { get; }
}
-}
\ No newline at end of file
+}
diff --git a/src/Umbraco.Web/Editors/EditorModelEventManager.cs b/src/Umbraco.Web/Editors/EditorModelEventManager.cs
index e2a248cb88..2225f5c577 100644
--- a/src/Umbraco.Web/Editors/EditorModelEventManager.cs
+++ b/src/Umbraco.Web/Editors/EditorModelEventManager.cs
@@ -1,4 +1,5 @@
-using System.Web.Http.Filters;
+using System.Collections.Generic;
+using System.Web.Http.Filters;
using Umbraco.Core.Events;
using Umbraco.Web.Models.ContentEditing;
@@ -13,6 +14,13 @@ namespace Umbraco.Web.Editors
public static event TypedEventHandler> SendingMediaModel;
public static event TypedEventHandler> SendingMemberModel;
public static event TypedEventHandler> SendingUserModel;
+ public static event TypedEventHandler>>> SendingDashboardModel;
+
+ private static void OnSendingDashboardModel(HttpActionExecutedContext sender, EditorModelEventArgs>> e)
+ {
+ var handler = SendingDashboardModel;
+ handler?.Invoke(sender, e);
+ }
private static void OnSendingUserModel(HttpActionExecutedContext sender, EditorModelEventArgs e)
{
@@ -56,6 +64,9 @@ namespace Umbraco.Web.Editors
if (e.Model is UserDisplay)
OnSendingUserModel(sender, new EditorModelEventArgs(e));
+
+ if (e.Model is IEnumerable>)
+ OnSendingDashboardModel(sender, new EditorModelEventArgs>>(e));
}
}
}
diff --git a/src/Umbraco.Web/Models/Mapping/RelationMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/RelationMapperProfile.cs
index 1622fb907e..e31b1877d3 100644
--- a/src/Umbraco.Web/Models/Mapping/RelationMapperProfile.cs
+++ b/src/Umbraco.Web/Models/Mapping/RelationMapperProfile.cs
@@ -11,18 +11,17 @@ namespace Umbraco.Web.Models.Mapping
{
// FROM IRelationType to RelationTypeDisplay
CreateMap()
- .ForMember(x => x.Icon, expression => expression.Ignore())
- .ForMember(x => x.Trashed, expression => expression.Ignore())
- .ForMember(x => x.Alias, expression => expression.Ignore())
- .ForMember(x => x.Path, expression => expression.Ignore())
- .ForMember(x => x.AdditionalData, expression => expression.Ignore())
- .ForMember(x => x.ChildObjectTypeName, expression => expression.Ignore())
- .ForMember(x => x.ParentObjectTypeName, expression => expression.Ignore())
- .ForMember(x => x.Relations, expression => expression.Ignore())
- .ForMember(
- x => x.Udi,
- expression => expression.MapFrom(
- content => Udi.Create(Constants.UdiEntityType.RelationType, content.Key)))
+ .ForMember(dest => dest.Icon, opt => opt.Ignore())
+ .ForMember(dest => dest.Trashed, opt => opt.Ignore())
+ .ForMember(dest => dest.Alias, opt => opt.Ignore())
+ .ForMember(dest => dest.Path, opt => opt.Ignore())
+ .ForMember(dest => dest.AdditionalData, opt => opt.Ignore())
+ .ForMember(dest => dest.ChildObjectTypeName, opt => opt.Ignore())
+ .ForMember(dest => dest.ParentObjectTypeName, opt => opt.Ignore())
+ .ForMember(dest => dest.Relations, opt => opt.Ignore())
+ .ForMember(dest => dest.ParentId, opt => opt.Ignore())
+ .ForMember(dest => dest.Notifications, opt => opt.Ignore())
+ .ForMember(dest => dest.Udi, opt => opt.MapFrom(content => Udi.Create(Constants.UdiEntityType.RelationType, content.Key)))
.AfterMap((src, dest) =>
{
// Build up the path
@@ -34,10 +33,15 @@ namespace Umbraco.Web.Models.Mapping
});
// FROM IRelation to RelationDisplay
- CreateMap();
+ CreateMap()
+ .ForMember(dest => dest.ParentName, opt => opt.Ignore())
+ .ForMember(dest => dest.ChildName, opt => opt.Ignore());
// FROM RelationTypeSave to IRelationType
- CreateMap();
+ CreateMap()
+ .ForMember(dest => dest.CreateDate, opt => opt.Ignore())
+ .ForMember(dest => dest.UpdateDate, opt => opt.Ignore())
+ .ForMember(dest => dest.DeleteDate, opt => opt.Ignore());
}
}
}
diff --git a/src/Umbraco.Web/WebApi/Filters/OutgoingEditorModelEventAttribute.cs b/src/Umbraco.Web/WebApi/Filters/OutgoingEditorModelEventAttribute.cs
index ec32b61bca..8410891a5d 100644
--- a/src/Umbraco.Web/WebApi/Filters/OutgoingEditorModelEventAttribute.cs
+++ b/src/Umbraco.Web/WebApi/Filters/OutgoingEditorModelEventAttribute.cs
@@ -19,16 +19,17 @@ namespace Umbraco.Web.WebApi.Filters
var user = UmbracoContext.Current.Security.CurrentUser;
if (user == null) return;
- var objectContent = actionExecutedContext.Response.Content as ObjectContent;
- if (objectContent != null)
+ if (actionExecutedContext.Response.Content is ObjectContent objectContent)
{
var model = objectContent.Value;
if (model != null)
{
- EditorModelEventManager.EmitEvent(actionExecutedContext, new EditorModelEventArgs(
- (dynamic)model,
- UmbracoContext.Current));
+ var args = new EditorModelEventArgs(
+ model,
+ UmbracoContext.Current);
+ EditorModelEventManager.EmitEvent(actionExecutedContext, args);
+ objectContent.Value = args.Model;
}
}