diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index eaabe44be3..4e19c6f25c 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -285,6 +285,7 @@ private void AddSerializationLayer() _services.AddScoped(typeof(AtomicOperationsResponseSerializer)); _services.AddScoped(sp => sp.GetRequiredService().GetSerializer()); _services.AddScoped(); + _services.AddSingleton(); _services.AddSingleton(); } diff --git a/src/JsonApiDotNetCore/Serialization/ETagGenerator.cs b/src/JsonApiDotNetCore/Serialization/ETagGenerator.cs index a16b8dc1cd..88648a70d2 100644 --- a/src/JsonApiDotNetCore/Serialization/ETagGenerator.cs +++ b/src/JsonApiDotNetCore/Serialization/ETagGenerator.cs @@ -1,6 +1,3 @@ -using System.Linq; -using System.Security.Cryptography; -using System.Text; using Microsoft.Net.Http.Headers; namespace JsonApiDotNetCore.Serialization @@ -8,39 +5,22 @@ namespace JsonApiDotNetCore.Serialization /// internal sealed class ETagGenerator : IETagGenerator { - private static readonly uint[] LookupTable = Enumerable.Range(0, 256).Select(ToLookupEntry).ToArray(); + private readonly IFingerprintGenerator _fingerprintGenerator; - private static uint ToLookupEntry(int index) + public ETagGenerator(IFingerprintGenerator fingerprintGenerator) { - string hex = index.ToString("X2"); - return hex[0] + ((uint)hex[1] << 16); + ArgumentGuard.NotNull(fingerprintGenerator, nameof(fingerprintGenerator)); + + _fingerprintGenerator = fingerprintGenerator; } /// public EntityTagHeaderValue Generate(string requestUrl, string responseBody) { - byte[] buffer = Encoding.UTF8.GetBytes(requestUrl + "|" + responseBody); - - using HashAlgorithm hashAlgorithm = MD5.Create(); - byte[] hash = hashAlgorithm.ComputeHash(buffer); + string fingerprint = _fingerprintGenerator.Generate(ArrayFactory.Create(requestUrl, responseBody)); + string eTagValue = "\"" + fingerprint + "\""; - string eTagValue = "\"" + ByteArrayToHex(hash) + "\""; return EntityTagHeaderValue.Parse(eTagValue); } - - // https://stackoverflow.com/questions/311165/how-do-you-convert-a-byte-array-to-a-hexadecimal-string-and-vice-versa - private static string ByteArrayToHex(byte[] bytes) - { - char[] buffer = new char[bytes.Length * 2]; - - for (int index = 0; index < bytes.Length; index++) - { - uint value = LookupTable[bytes[index]]; - buffer[2 * index] = (char)value; - buffer[2 * index + 1] = (char)(value >> 16); - } - - return new string(buffer); - } } } diff --git a/src/JsonApiDotNetCore/Serialization/FingerprintGenerator.cs b/src/JsonApiDotNetCore/Serialization/FingerprintGenerator.cs new file mode 100644 index 0000000000..3c1083dfbe --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/FingerprintGenerator.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +namespace JsonApiDotNetCore.Serialization +{ + /// + internal sealed class FingerprintGenerator : IFingerprintGenerator + { + private static readonly byte[] Separator = Encoding.UTF8.GetBytes("|"); + private static readonly uint[] LookupTable = Enumerable.Range(0, 256).Select(ToLookupEntry).ToArray(); + + private static uint ToLookupEntry(int index) + { + string hex = index.ToString("X2"); + return hex[0] + ((uint)hex[1] << 16); + } + + /// + public string Generate(IEnumerable elements) + { + ArgumentGuard.NotNull(elements, nameof(elements)); + + using var hasher = IncrementalHash.CreateHash(HashAlgorithmName.MD5); + + foreach (string element in elements) + { + byte[] buffer = Encoding.UTF8.GetBytes(element); + hasher.AppendData(buffer); + hasher.AppendData(Separator); + } + + byte[] hash = hasher.GetHashAndReset(); + return ByteArrayToHex(hash); + } + + private static string ByteArrayToHex(byte[] bytes) + { + // https://stackoverflow.com/questions/311165/how-do-you-convert-a-byte-array-to-a-hexadecimal-string-and-vice-versa + + char[] buffer = new char[bytes.Length * 2]; + + for (int index = 0; index < bytes.Length; index++) + { + uint value = LookupTable[bytes[index]]; + buffer[2 * index] = (char)value; + buffer[2 * index + 1] = (char)(value >> 16); + } + + return new string(buffer); + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/IFingerprintGenerator.cs b/src/JsonApiDotNetCore/Serialization/IFingerprintGenerator.cs new file mode 100644 index 0000000000..51fafaf650 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/IFingerprintGenerator.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using JetBrains.Annotations; + +namespace JsonApiDotNetCore.Serialization +{ + /// + /// Provides a method to generate a fingerprint for a collection of string values. + /// + [PublicAPI] + public interface IFingerprintGenerator + { + /// + /// Generates a fingerprint for the specified elements. + /// + public string Generate(IEnumerable elements); + } +} diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs index 71ebee0a3f..bcf0e5bcaa 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs @@ -86,7 +86,7 @@ public async Task ReadAsync(InputFormatterContext context) private async Task GetRequestBodyAsync(Stream bodyStream) { - using var reader = new StreamReader(bodyStream); + using var reader = new StreamReader(bodyStream, leaveOpen: true); return await reader.ReadToEndAsync(); } diff --git a/test/TestBuildingBlocks/IntegrationTest.cs b/test/TestBuildingBlocks/IntegrationTest.cs index 86dc7d4fb9..9e21f841b0 100644 --- a/test/TestBuildingBlocks/IntegrationTest.cs +++ b/test/TestBuildingBlocks/IntegrationTest.cs @@ -1,6 +1,7 @@ using System; using System.Net.Http; using System.Net.Http.Headers; +using System.Text; using System.Threading.Tasks; using JsonApiDotNetCore.Middleware; using Newtonsoft.Json; @@ -60,6 +61,7 @@ public abstract class IntegrationTest { requestText = requestText.Replace("atomic__", "atomic:"); request.Content = new StringContent(requestText); + request.Content.Headers.ContentLength = Encoding.UTF8.GetByteCount(requestText); if (contentType != null) {