diff --git a/src/Umbraco.Core/HashCodeCombiner.cs b/src/Umbraco.Core/HashCodeCombiner.cs
index d3a55d5256..5f99c65787 100644
--- a/src/Umbraco.Core/HashCodeCombiner.cs
+++ b/src/Umbraco.Core/HashCodeCombiner.cs
@@ -2,17 +2,19 @@
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Text;
namespace Umbraco.Core
{
///
- /// Used to create a hash code from multiple objects.
+ /// Used to create a .NET HashCode from multiple objects.
///
///
/// .Net has a class the same as this: System.Web.Util.HashCodeCombiner and of course it works for all sorts of things
/// which we've not included here as we just need a quick easy class for this in order to create a unique
/// hash of directories/files to see if they have changed.
+ ///
+ /// NOTE: It's probably best to not relying on the hashing result across AppDomains! If you need a constant/reliable hash value
+ /// between AppDomains use SHA1. This is perfect for hashing things in a very fast way for a single AppDomain.
///
internal class HashCodeCombiner
{
diff --git a/src/Umbraco.Core/HashGenerator.cs b/src/Umbraco.Core/HashGenerator.cs
new file mode 100644
index 0000000000..7306dc9045
--- /dev/null
+++ b/src/Umbraco.Core/HashGenerator.cs
@@ -0,0 +1,154 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace Umbraco.Core
+{
+ ///
+ /// Used to generate a string hash using crypto libraries over multiple objects
+ ///
+ ///
+ /// This should be used to generate a reliable hash that survives AppDomain restarts.
+ /// This will use the crypto libs to generate the hash and will try to ensure that
+ /// strings, etc... are not re-allocated so it's not consuming much memory.
+ ///
+ internal class HashGenerator : DisposableObject
+ {
+ public HashGenerator()
+ {
+ _writer = new StreamWriter(_ms, Encoding.Unicode, 1024, leaveOpen: true);
+ }
+
+ private readonly MemoryStream _ms = new MemoryStream();
+ private StreamWriter _writer;
+
+ internal void AddInt(int i)
+ {
+ _writer.Write(i);
+ }
+
+ internal void AddLong(long i)
+ {
+ _writer.Write(i);
+ }
+
+ internal void AddObject(object o)
+ {
+ _writer.Write(o);
+ }
+
+ internal void AddDateTime(DateTime d)
+ {
+ _writer.Write(d.Ticks);;
+ }
+
+ internal void AddString(string s)
+ {
+ if (s != null)
+ _writer.Write(s);
+ }
+
+ internal void AddCaseInsensitiveString(string s)
+ {
+ //I've tried to no allocate a new string with this which can be done if we use the CompareInfo.GetSortKey method which will create a new
+ //byte array that we can use to write to the output, however this also allocates new objects so i really don't think the performance
+ //would be much different. In any case, i'll leave this here for reference. We could write the bytes out based on the sort key,
+ //this is how we could deal with case insensitivity without allocating another string
+ //for reference see: https://stackoverflow.com/a/10452967/694494
+ //we could go a step further and s.Normalize() but we're not really dealing with crazy unicode with this class so far.
+
+ if (s != null)
+ _writer.Write(s.ToUpperInvariant());
+ }
+
+ internal void AddFileSystemItem(FileSystemInfo f)
+ {
+ //if it doesn't exist, don't proceed.
+ if (f.Exists == false)
+ return;
+
+ AddCaseInsensitiveString(f.FullName);
+ AddDateTime(f.CreationTimeUtc);
+ AddDateTime(f.LastWriteTimeUtc);
+
+ //check if it is a file or folder
+ var fileInfo = f as FileInfo;
+ if (fileInfo != null)
+ {
+ AddLong(fileInfo.Length);
+ }
+
+ var dirInfo = f as DirectoryInfo;
+ if (dirInfo != null)
+ {
+ foreach (var d in dirInfo.GetFiles())
+ {
+ AddFile(d);
+ }
+ foreach (var s in dirInfo.GetDirectories())
+ {
+ AddFolder(s);
+ }
+ }
+ }
+
+ internal void AddFile(FileInfo f)
+ {
+ AddFileSystemItem(f);
+ }
+
+ internal void AddFolder(DirectoryInfo d)
+ {
+ AddFileSystemItem(d);
+ }
+
+ ///
+ /// Returns the generated hash output of all added objects
+ ///
+ ///
+ internal string GenerateHash()
+ {
+ //flush,close,dispose the writer,then create a new one since it's possible to keep adding after GenerateHash is called.
+
+ _writer.Flush();
+ _writer.Close();
+ _writer.Dispose();
+ _writer = new StreamWriter(_ms, Encoding.UTF8, 1024, leaveOpen: true);
+
+ var hashType = CryptoConfig.AllowOnlyFipsAlgorithms ? "SHA1" : "MD5";
+
+ //create an instance of the correct hashing provider based on the type passed in
+ var hasher = HashAlgorithm.Create(hashType);
+ if (hasher == null) throw new InvalidOperationException("No hashing type found by name " + hashType);
+ using (hasher)
+ {
+ var buffer = _ms.GetBuffer();
+ //get the hashed values created by our selected provider
+ var hashedByteArray = hasher.ComputeHash(buffer);
+
+ //create a StringBuilder object
+ var stringBuilder = new StringBuilder();
+
+ //loop to each each byte
+ foreach (var b in hashedByteArray)
+ {
+ //append it to our StringBuilder
+ stringBuilder.Append(b.ToString("x2"));
+ }
+
+ //return the hashed value
+ return stringBuilder.ToString();
+ }
+ }
+
+ protected override void DisposeResources()
+ {
+ _writer.Close();
+ _writer.Dispose();
+ _ms.Close();
+ _ms.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/PluginManager.cs b/src/Umbraco.Core/PluginManager.cs
index fe9236c58c..30029d2c87 100644
--- a/src/Umbraco.Core/PluginManager.cs
+++ b/src/Umbraco.Core/PluginManager.cs
@@ -44,8 +44,8 @@ namespace Umbraco.Core
private readonly object _typesLock = new object();
private readonly Dictionary _types = new Dictionary();
- private long _cachedAssembliesHash = -1;
- private long _currentAssembliesHash = -1;
+ private string _cachedAssembliesHash = null;
+ private string _currentAssembliesHash = null;
private IEnumerable _assemblies;
private bool _reportedChange;
@@ -75,9 +75,9 @@ namespace Umbraco.Core
if (detectChanges)
{
- //first check if the cached hash is 0, if it is then we ne
+ //first check if the cached hash is string.Empty, if it is then we need
//do the check if they've changed
- RequiresRescanning = (CachedAssembliesHash != CurrentAssembliesHash) || CachedAssembliesHash == 0;
+ RequiresRescanning = (CachedAssembliesHash != CurrentAssembliesHash) || CachedAssembliesHash == string.Empty;
//if they have changed, we need to write the new file
if (RequiresRescanning)
{
@@ -180,23 +180,20 @@ namespace Umbraco.Core
///
/// Gets the currently cached hash value of the scanned assemblies.
///
- /// The cached hash value, or 0 if no cache is found.
- internal long CachedAssembliesHash
+ /// The cached hash value, or string.Empty if no cache is found.
+ internal string CachedAssembliesHash
{
get
{
- if (_cachedAssembliesHash != -1)
+ if (_cachedAssembliesHash != null)
return _cachedAssembliesHash;
var filePath = GetPluginHashFilePath();
- if (File.Exists(filePath) == false) return 0;
+ if (File.Exists(filePath) == false) return string.Empty;
var hash = File.ReadAllText(filePath, Encoding.UTF8);
-
- long val;
- if (long.TryParse(hash, out val) == false) return 0;
-
- _cachedAssembliesHash = val;
+
+ _cachedAssembliesHash = hash;
return _cachedAssembliesHash;
}
}
@@ -205,11 +202,11 @@ namespace Umbraco.Core
/// Gets the current assemblies hash based on creating a hash from the assemblies in various places.
///
/// The current hash.
- internal long CurrentAssembliesHash
+ internal string CurrentAssembliesHash
{
get
{
- if (_currentAssembliesHash != -1)
+ if (_currentAssembliesHash != null)
return _currentAssembliesHash;
_currentAssembliesHash = GetFileHash(new List>
@@ -245,41 +242,40 @@ namespace Umbraco.Core
/// The hash.
/// Each file is a tuple containing the FileInfo object and a boolean which indicates whether to hash the
/// file properties (false) or the file contents (true).
- internal static long GetFileHash(IEnumerable> filesAndFolders, ProfilingLogger logger)
+ internal static string GetFileHash(IEnumerable> filesAndFolders, ProfilingLogger logger)
{
using (logger.TraceDuration("Determining hash of code files on disk", "Hash determined"))
- {
- var hashCombiner = new HashCodeCombiner();
-
+ {
// get the distinct file infos to hash
var uniqInfos = new HashSet();
var uniqContent = new HashSet();
-
- foreach (var fileOrFolder in filesAndFolders)
+ using (var generator = new HashGenerator())
{
- var info = fileOrFolder.Item1;
- if (fileOrFolder.Item2)
+ foreach (var fileOrFolder in filesAndFolders)
{
- // add each unique file's contents to the hash
- // normalize the content for cr/lf and case-sensitivity
-
- if (uniqContent.Contains(info.FullName)) continue;
- uniqContent.Add(info.FullName);
- if (File.Exists(info.FullName) == false) continue;
- var content = RemoveCrLf(File.ReadAllText(info.FullName));
- hashCombiner.AddCaseInsensitiveString(content);
+ var info = fileOrFolder.Item1;
+ if (fileOrFolder.Item2)
+ {
+ // add each unique file's contents to the hash
+ // normalize the content for cr/lf and case-sensitivity
+ if (uniqContent.Add(info.FullName))
+ {
+ if (File.Exists(info.FullName) == false) continue;
+ var content = RemoveCrLf(File.ReadAllText(info.FullName));
+ generator.AddCaseInsensitiveString(content);
+ }
+ }
+ else
+ {
+ // add each unique folder/file to the hash
+ if (uniqInfos.Add(info.FullName))
+ {
+ generator.AddFileSystemItem(info);
+ }
+ }
}
- else
- {
- // add each unique folder/file to the hash
-
- if (uniqInfos.Contains(info.FullName)) continue;
- uniqInfos.Add(info.FullName);
- hashCombiner.AddFileSystemItem(info);
- }
- }
-
- return ConvertHashToInt64(hashCombiner.GetCombinedHashCode());
+ return generator.GenerateHash();
+ }
}
}
@@ -303,34 +299,25 @@ namespace Umbraco.Core
/// A collection of files.
/// A profiling logger.
/// The hash.
- internal static long GetFileHash(IEnumerable filesAndFolders, ProfilingLogger logger)
+ internal static string GetFileHash(IEnumerable filesAndFolders, ProfilingLogger logger)
{
using (logger.TraceDuration("Determining hash of code files on disk", "Hash determined"))
{
- var hashCombiner = new HashCodeCombiner();
-
- // get the distinct file infos to hash
- var uniqInfos = new HashSet();
-
- foreach (var fileOrFolder in filesAndFolders)
+ using (var generator = new HashGenerator())
{
- if (uniqInfos.Contains(fileOrFolder.FullName)) continue;
- uniqInfos.Add(fileOrFolder.FullName);
- hashCombiner.AddFileSystemItem(fileOrFolder);
- }
+ // get the distinct file infos to hash
+ var uniqInfos = new HashSet();
- return ConvertHashToInt64(hashCombiner.GetCombinedHashCode());
+ foreach (var fileOrFolder in filesAndFolders)
+ {
+ if (uniqInfos.Contains(fileOrFolder.FullName)) continue;
+ uniqInfos.Add(fileOrFolder.FullName);
+ generator.AddFileSystemItem(fileOrFolder);
+ }
+ return generator.GenerateHash();
+ }
}
- }
-
- ///
- /// Converts a string hash value into an Int64.
- ///
- internal static long ConvertHashToInt64(string val)
- {
- long outVal;
- return long.TryParse(val, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out outVal) ? outVal : 0;
- }
+ }
#endregion
diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs
index c8d93c6367..e5cbe1650b 100644
--- a/src/Umbraco.Core/StringExtensions.cs
+++ b/src/Umbraco.Core/StringExtensions.cs
@@ -759,7 +759,7 @@ namespace Umbraco.Core
foreach (var b in hashedByteArray)
{
//append it to our StringBuilder
- stringBuilder.Append(b.ToString("x2").ToLower());
+ stringBuilder.Append(b.ToString("x2"));
}
//return the hashed value
diff --git a/src/Umbraco.Core/Sync/WebServiceServerMessenger.cs b/src/Umbraco.Core/Sync/WebServiceServerMessenger.cs
index e5717fbfdc..09b845d29a 100644
--- a/src/Umbraco.Core/Sync/WebServiceServerMessenger.cs
+++ b/src/Umbraco.Core/Sync/WebServiceServerMessenger.cs
@@ -126,10 +126,12 @@ namespace Umbraco.Core.Sync
public static string GetServerHash(string machineName, string appDomainAppId)
{
- var hasher = new HashCodeCombiner();
- hasher.AddCaseInsensitiveString(appDomainAppId);
- hasher.AddCaseInsensitiveString(machineName);
- return hasher.GetCombinedHashCode();
+ using (var generator = new HashGenerator())
+ {
+ generator.AddString(machineName);
+ generator.AddString(appDomainAppId);
+ return generator.GenerateHash();
+ }
}
protected override bool RequiresDistributed(IEnumerable servers, ICacheRefresher refresher, MessageType messageType)
diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj
index fcc2e5bb70..ec6bfebd5b 100644
--- a/src/Umbraco.Core/Umbraco.Core.csproj
+++ b/src/Umbraco.Core/Umbraco.Core.csproj
@@ -342,6 +342,7 @@
+
diff --git a/src/Umbraco.Tests/Cache/CacheRefresherTests.cs b/src/Umbraco.Tests/Cache/CacheRefresherTests.cs
index 33dfd16743..189848651a 100644
--- a/src/Umbraco.Tests/Cache/CacheRefresherTests.cs
+++ b/src/Umbraco.Tests/Cache/CacheRefresherTests.cs
@@ -9,10 +9,10 @@ namespace Umbraco.Tests.Cache
public class CacheRefresherTests
{
[TestCase("", "123456", "testmachine", true)] //empty hash will continue
- [TestCase("fffffff28449cf33", "123456", "testmachine", false)] //match, don't continue
- [TestCase("fffffff28449cf33", "12345", "testmachine", true)] // no match, continue
- [TestCase("fffffff28449cf33", "123456", "testmachin", true)] // same
- [TestCase("fffffff28449cf3", "123456", "testmachine", true)] // same
+ [TestCase("2e6deefea4444a69dbd15a01b4c2749d", "123456", "testmachine", false)] //match, don't continue
+ [TestCase("2e6deefea4444a69dbd15a01b4c2749d", "12345", "testmachine", true)] // no match, continue
+ [TestCase("2e6deefea4444a69dbd15a01b4c2749d", "123456", "testmachin", true)] // same
+ [TestCase("2e6deefea4444a69dbd15a01b4c2749", "123456", "testmachine", true)] // same
public void Continue_Refreshing_For_Request(string hash, string appDomainAppId, string machineName, bool expected)
{
if (expected)
diff --git a/src/Umbraco.Tests/HashCodeCombinerTests.cs b/src/Umbraco.Tests/HashCodeCombinerTests.cs
index 8d1ad021dd..08eacd655b 100644
--- a/src/Umbraco.Tests/HashCodeCombinerTests.cs
+++ b/src/Umbraco.Tests/HashCodeCombinerTests.cs
@@ -6,7 +6,7 @@ using Umbraco.Core;
namespace Umbraco.Tests
{
- [TestFixture]
+ [TestFixture]
public class HashCodeCombinerTests
{
diff --git a/src/Umbraco.Tests/HashGeneratorTests.cs b/src/Umbraco.Tests/HashGeneratorTests.cs
new file mode 100644
index 0000000000..179b4eaeaa
--- /dev/null
+++ b/src/Umbraco.Tests/HashGeneratorTests.cs
@@ -0,0 +1,184 @@
+using System;
+using System.IO;
+using System.Reflection;
+using NUnit.Framework;
+using Umbraco.Core;
+
+namespace Umbraco.Tests
+{
+ [TestFixture]
+ public class HashGeneratorTests
+ {
+ private string Generate(bool isCaseSensitive, params string[] strs)
+ {
+ using (var generator = new HashGenerator())
+ {
+ foreach (var str in strs)
+ {
+ if (isCaseSensitive)
+ generator.AddString(str);
+ else
+ generator.AddCaseInsensitiveString(str);
+ }
+ return generator.GenerateHash();
+ }
+ }
+
+ [Test]
+ public void Generate_Hash_Multiple_Strings_Case_Sensitive()
+ {
+
+ var hash1 = Generate(true, "hello", "world");
+ var hash2 = Generate(true, "hello", "world");
+ var hashFalse1 = Generate(true, "hello", "worlD");
+ var hashFalse2 = Generate(true, "hEllo", "world");
+
+ Assert.AreEqual(hash1, hash2);
+ Assert.AreNotEqual(hash1, hashFalse1);
+ Assert.AreNotEqual(hash1, hashFalse2);
+ }
+
+ [Test]
+ public void Generate_Hash_Multiple_Strings_Case_Insensitive()
+ {
+ var hash1 = Generate(false, "hello", "world");
+ var hash2 = Generate(false, "hello", "world");
+ var hashFalse1 = Generate(false, "hello", "worlD");
+ var hashFalse2 = Generate(false, "hEllo", "world");
+
+ Assert.AreEqual(hash1, hash2);
+ Assert.AreEqual(hash1, hashFalse1);
+ Assert.AreEqual(hash1, hashFalse2);
+ }
+
+ private DirectoryInfo PrepareFolder()
+ {
+ var assDir = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory;
+ var dir = Directory.CreateDirectory(Path.Combine(assDir.FullName, "HashCombiner", Guid.NewGuid().ToString("N")));
+ foreach (var f in dir.GetFiles())
+ {
+ f.Delete();
+ }
+ return dir;
+ }
+
+ [Test]
+ public void HashCombiner_Test_String()
+ {
+ using (var combiner1 = new HashGenerator())
+ using (var combiner2 = new HashGenerator())
+ {
+ combiner1.AddCaseInsensitiveString("Hello");
+ combiner2.AddCaseInsensitiveString("hello");
+ Assert.AreEqual(combiner1.GenerateHash(), combiner2.GenerateHash());
+ combiner2.AddCaseInsensitiveString("world");
+ Assert.AreNotEqual(combiner1.GenerateHash(), combiner2.GenerateHash());
+ }
+
+ }
+
+ [Test]
+ public void HashCombiner_Test_Int()
+ {
+ using (var combiner1 = new HashGenerator())
+ using (var combiner2 = new HashGenerator())
+ {
+ combiner1.AddInt(1234);
+ combiner2.AddInt(1234);
+ Assert.AreEqual(combiner1.GenerateHash(), combiner2.GenerateHash());
+ combiner2.AddInt(1);
+ Assert.AreNotEqual(combiner1.GenerateHash(), combiner2.GenerateHash());
+ }
+ }
+
+ [Test]
+ public void HashCombiner_Test_DateTime()
+ {
+ using (var combiner1 = new HashGenerator())
+ using (var combiner2 = new HashGenerator())
+ {
+ var dt = DateTime.Now;
+ combiner1.AddDateTime(dt);
+ combiner2.AddDateTime(dt);
+ Assert.AreEqual(combiner1.GenerateHash(), combiner2.GenerateHash());
+ combiner2.AddDateTime(DateTime.Now);
+ Assert.AreNotEqual(combiner1.GenerateHash(), combiner2.GenerateHash());
+ }
+ }
+
+ [Test]
+ public void HashCombiner_Test_File()
+ {
+ using (var combiner1 = new HashGenerator())
+ using (var combiner2 = new HashGenerator())
+ using (var combiner3 = new HashGenerator())
+ {
+ var dir = PrepareFolder();
+ var file1Path = Path.Combine(dir.FullName, "hastest1.txt");
+ File.Delete(file1Path);
+ using (var file1 = File.CreateText(Path.Combine(dir.FullName, "hastest1.txt")))
+ {
+ file1.WriteLine("hello");
+ }
+ var file2Path = Path.Combine(dir.FullName, "hastest2.txt");
+ File.Delete(file2Path);
+ using (var file2 = File.CreateText(Path.Combine(dir.FullName, "hastest2.txt")))
+ {
+ //even though files are the same, the dates are different
+ file2.WriteLine("hello");
+ }
+
+ combiner1.AddFile(new FileInfo(file1Path));
+
+ combiner2.AddFile(new FileInfo(file1Path));
+
+ combiner3.AddFile(new FileInfo(file2Path));
+
+ Assert.AreEqual(combiner1.GenerateHash(), combiner2.GenerateHash());
+ Assert.AreNotEqual(combiner1.GenerateHash(), combiner3.GenerateHash());
+
+ combiner2.AddFile(new FileInfo(file2Path));
+
+ Assert.AreNotEqual(combiner1.GenerateHash(), combiner2.GenerateHash());
+ }
+ }
+
+ [Test]
+ public void HashCombiner_Test_Folder()
+ {
+ using (var combiner1 = new HashGenerator())
+ using (var combiner2 = new HashGenerator())
+ using (var combiner3 = new HashGenerator())
+ {
+ var dir = PrepareFolder();
+ var file1Path = Path.Combine(dir.FullName, "hastest1.txt");
+ File.Delete(file1Path);
+ using (var file1 = File.CreateText(Path.Combine(dir.FullName, "hastest1.txt")))
+ {
+ file1.WriteLine("hello");
+ }
+
+ //first test the whole folder
+ combiner1.AddFolder(dir);
+
+ combiner2.AddFolder(dir);
+
+ Assert.AreEqual(combiner1.GenerateHash(), combiner2.GenerateHash());
+
+ //now add a file to the folder
+
+ var file2Path = Path.Combine(dir.FullName, "hastest2.txt");
+ File.Delete(file2Path);
+ using (var file2 = File.CreateText(Path.Combine(dir.FullName, "hastest2.txt")))
+ {
+ //even though files are the same, the dates are different
+ file2.WriteLine("hello");
+ }
+
+ combiner3.AddFolder(dir);
+
+ Assert.AreNotEqual(combiner1.GenerateHash(), combiner3.GenerateHash());
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs
index 333970bf67..15d7a7ffda 100644
--- a/src/Umbraco.Tests/Plugins/PluginManagerTests.cs
+++ b/src/Umbraco.Tests/Plugins/PluginManagerTests.cs
@@ -218,15 +218,7 @@ AnotherContentFinder
//ensure they are all found
Assert.IsTrue(plugins.Result.ContainsAll(shouldContain));
}
-
- [Test]
- public void PluginHash_From_String()
- {
- var s = "hello my name is someone".GetHashCode().ToString("x", CultureInfo.InvariantCulture);
- var output = PluginManager.ConvertHashToInt64(s);
- Assert.AreNotEqual(0, output);
- }
-
+
[Test]
public void Get_Plugins_Hash()
{
diff --git a/src/Umbraco.Tests/Strings/StringExtensionsTests.cs b/src/Umbraco.Tests/Strings/StringExtensionsTests.cs
index 199bd9254d..e0e80f3ab2 100644
--- a/src/Umbraco.Tests/Strings/StringExtensionsTests.cs
+++ b/src/Umbraco.Tests/Strings/StringExtensionsTests.cs
@@ -24,7 +24,7 @@ namespace Umbraco.Tests.Strings
{
ShortStringHelperResolver.Reset();
}
-
+
[TestCase("hello", "world", false)]
[TestCase("hello", "hello", true)]
[TestCase("hellohellohellohellohellohellohello", "hellohellohellohellohellohellohelloo", false)]
diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj
index 6859cb899f..ebee151364 100644
--- a/src/Umbraco.Tests/Umbraco.Tests.csproj
+++ b/src/Umbraco.Tests/Umbraco.Tests.csproj
@@ -166,6 +166,7 @@
+