Browse Source

Performance improvements (#1011)

* Improve performance.

* Minor formatting.

* Mini improvements.

* Fix hasher.
pull/1012/head
Sebastian Stehle 3 years ago
committed by GitHub
parent
commit
10544d2f9d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/TranslationStatus.cs
  2. 5
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/DefaultValueFactory.cs
  3. 9
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ExcludeChangedTypes.cs
  4. 21
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ResolveLanguages.cs
  5. 3
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs
  6. 10
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueValidator.cs
  7. 5
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs
  8. 4
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/Steps/CalculateTokens.cs
  9. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs
  10. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs
  11. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/CalculateTokens.cs
  12. 4
      backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs
  13. 252
      backend/src/Squidex.Infrastructure/HashExtensions.cs
  14. 12
      backend/src/Squidex.Infrastructure/Json/IJsonSerializer.cs
  15. 2
      backend/src/Squidex.Infrastructure/Json/Objects/JsonValueType.cs
  16. 20
      backend/src/Squidex.Infrastructure/Json/System/SystemJsonSerializer.cs
  17. 59
      backend/src/Squidex.Infrastructure/RandomHash.cs
  18. 119
      backend/src/Squidex.Infrastructure/Reflection/Internal/PropertyAccessor.cs
  19. 297
      backend/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs
  20. 22
      backend/src/Squidex.Infrastructure/StringExtensions.cs
  21. 11
      backend/src/Squidex.Web/ETagExtensions.cs
  22. 18
      backend/src/Squidex.Web/Pipeline/CachingManager.cs
  23. 4
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs
  24. 182
      backend/tests/Squidex.Infrastructure.Tests/HashExtensionsTests.cs
  25. 98
      backend/tests/Squidex.Infrastructure.Tests/Reflection/PropertiesTypeAccessorTests.cs
  26. 153
      backend/tests/Squidex.Infrastructure.Tests/Reflection/PropertyAccessorTests.cs
  27. 16
      backend/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs

2
backend/src/Squidex.Domain.Apps.Core.Model/Contents/TranslationStatus.cs

@ -62,6 +62,6 @@ public class TranslationStatus : Dictionary<string, int>
private static bool IsValidValue(JsonValue? value)
{
return value != null && value.Value.Type != JsonValueType.Null;
return value != null && value.Value.Value != null;
}
}

5
backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/DefaultValueFactory.cs

@ -27,10 +27,9 @@ public sealed class DefaultValueFactory : IFieldPropertiesVisitor<JsonValue, Def
public static JsonValue CreateDefaultValue(IField field, Instant now, string partition)
{
Guard.NotNull(field);
Guard.NotNull(partition);
var args = new Args(now, partition);
return field.RawProperties.Accept(Instance, new Args(now, partition));
return field.RawProperties.Accept(Instance, args);
}
public JsonValue Visit(ArrayFieldProperties properties, Args args)

9
backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ExcludeChangedTypes.cs

@ -52,13 +52,6 @@ public sealed class ExcludeChangedTypes : IContentFieldConverter, IContentValueC
private bool IsChangedType(IField field, JsonValue source)
{
try
{
return !JsonValueValidator.IsValid(field, source, serializer);
}
catch
{
return true;
}
return !JsonValueValidator.IsValid(field, source, serializer);
}
}

21
backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ResolveLanguages.cs

@ -14,14 +14,13 @@ namespace Squidex.Domain.Apps.Core.ConvertContent;
public sealed class ResolveLanguages : IContentFieldConverter
{
private readonly LanguagesConfig languages;
private readonly HashSet<string> languageCodes;
private readonly Dictionary<string, string[]> languagesWithFallbacks;
public bool ResolveFallback { get; init; }
public ResolveLanguages(LanguagesConfig languages, params Language[] filteredLanguages)
{
this.languages = languages;
HashSet<string> languageCodes;
if (filteredLanguages?.Length > 0)
{
@ -36,6 +35,11 @@ public sealed class ResolveLanguages : IContentFieldConverter
{
languageCodes.Add(languages.Master);
}
languagesWithFallbacks =
languageCodes.ToDictionary(
language => language,
language => languages.GetPriorities(language).Where(l => l != language).ToArray());
}
public ContentFieldData? ConvertFieldAfter(IRootField field, ContentFieldData source)
@ -47,20 +51,15 @@ public sealed class ResolveLanguages : IContentFieldConverter
if (ResolveFallback)
{
foreach (var languageCode in languageCodes)
foreach (var (languageCode, fallbacks) in languagesWithFallbacks)
{
if (source.TryGetNonNull(languageCode, out _))
{
continue;
}
foreach (var fallback in languages.GetPriorities(languageCode))
foreach (var fallback in fallbacks)
{
if (fallback == languageCode)
{
continue;
}
if (source.TryGetNonNull(fallback, out var fallbackValue))
{
source[languageCode] = fallbackValue;
@ -76,7 +75,7 @@ public sealed class ResolveLanguages : IContentFieldConverter
foreach (var (key, _) in source)
{
if (!languageCodes.Contains(key))
if (!languagesWithFallbacks.ContainsKey(key))
{
source.Remove(key);
isRemoved = true;

3
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs

@ -31,9 +31,6 @@ public sealed class JsonValueConverter : IFieldPropertiesVisitor<(object? Result
public static (object? Result, JsonError? Error) ConvertValue(IField field, JsonValue value, IJsonSerializer serializer,
ResolvedComponents components)
{
Guard.NotNull(field);
Guard.NotNull(value);
var args = new Args(value, serializer, components);
return field.RawProperties.Accept(Instance, args);

10
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueValidator.cs

@ -117,6 +117,11 @@ public sealed class JsonValueValidator : IFieldPropertiesVisitor<bool, JsonValue
return false;
}
if (a.Count == 0)
{
return true;
}
foreach (var item in a)
{
if (item.Value is not string)
@ -135,6 +140,11 @@ public sealed class JsonValueValidator : IFieldPropertiesVisitor<bool, JsonValue
return false;
}
if (a.Count == 0)
{
return true;
}
foreach (var item in a)
{
if (item.Value is not JsonObject)

5
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs

@ -7,6 +7,7 @@
using Squidex.Infrastructure;
using Squidex.Infrastructure.Reflection;
using System.Diagnostics;
namespace Squidex.Domain.Apps.Entities.Assets.Queries;
@ -36,7 +37,7 @@ public sealed class AssetEnricher : IAssetEnricher
Guard.NotNull(assets);
Guard.NotNull(context);
using (Telemetry.Activities.StartActivity("AssetEnricher/EnrichAsync"))
using (var activity = Telemetry.Activities.StartActivity("AssetEnricher/EnrichAsync"))
{
var results = new List<AssetEntity>();
@ -73,6 +74,8 @@ public sealed class AssetEnricher : IAssetEnricher
}
}
activity?.SetTag("numItems", results.Count);
return results;
}
}

4
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/Steps/CalculateTokens.cs

@ -42,9 +42,9 @@ public sealed class CalculateTokens : IAssetEnricherStep
u = url
};
var json = serializer.Serialize(token);
var json = serializer.SerializeToBytes(token);
asset.EditToken = Convert.ToBase64String(Encoding.UTF8.GetBytes(json));
asset.EditToken = Convert.ToBase64String(json);
}
return Task.CompletedTask;

4
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs

@ -232,9 +232,9 @@ internal static class ContentFields
private static string? GetSchemaName(JsonObject component)
{
if (component.TryGetValue("schemaName", out var name) && name.Type == JsonValueType.String)
if (component.TryGetValue("schemaName", out var value) && value.Value is string name)
{
return name.ToString();
return name;
}
return null;

4
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs

@ -46,7 +46,7 @@ public sealed class ContentEnricher : IContentEnricher
private async Task<IReadOnlyList<IEnrichedContentEntity>> EnrichInternalAsync(IEnumerable<IContentEntity> contents, bool cloneData, Context context,
CancellationToken ct)
{
using (Telemetry.Activities.StartActivity("ContentEnricher/EnrichInternalAsync"))
using (var activity = Telemetry.Activities.StartActivity("ContentEnricher/EnrichInternalAsync"))
{
var results = new List<ContentEntity>();
@ -110,6 +110,8 @@ public sealed class ContentEnricher : IContentEnricher
}
}
activity?.SetTag("numItems", results.Count);
return results;
}
}

4
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/CalculateTokens.cs

@ -38,9 +38,9 @@ public sealed class CalculateTokens : IContentEnricherStep
u = url
};
var json = serializer.Serialize(token);
var json = serializer.SerializeToBytes(token);
content.EditToken = Convert.ToBase64String(Encoding.UTF8.GetBytes(json));
content.EditToken = Convert.ToBase64String(json);
}
return Task.CompletedTask;

4
backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/Formatter.cs

@ -73,8 +73,8 @@ public static class Formatter
{
var payload = Encoding.UTF8.GetBytes(eventData.Payload);
var headersJson = serializer.Serialize(eventData.Headers);
var headersBytes = Encoding.UTF8.GetBytes(headersJson);
var headersJson = serializer.SerializeToBytes(eventData.Headers);
var headersBytes = headersJson;
return new EventStoreData(Uuid.FromGuid(Guid.NewGuid()), eventData.Type, payload, headersBytes);
}

252
backend/src/Squidex.Infrastructure/HashExtensions.cs

@ -0,0 +1,252 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Buffers;
using System.Security.Cryptography;
using System.Text;
namespace Squidex.Infrastructure;
public static class HashExtensions
{
private const int MaxStackSize = 128;
private delegate int HashMethod(ReadOnlySpan<byte> source, Span<byte> destination);
private delegate string EncodeMethod(ReadOnlySpan<byte> source);
private static readonly HashMethod HashMD5 = MD5.HashData;
private static readonly HashMethod HashSHA256 = SHA256.HashData;
private static readonly HashMethod HashSHA512 = SHA512.HashData;
private static readonly EncodeMethod EncodeUTF8 = Encoding.UTF8.GetString;
private static readonly EncodeMethod EncodeBase64 = input => Convert.ToBase64String(input, default);
public static string ToSha256Base64(this string value)
{
return ToHashed(value, HashSHA256, SHA256.HashSizeInBits, Encoding.UTF8, EncodeBase64);
}
public static string ToSha256Base64(this ReadOnlySpan<byte> value)
{
return ToHashed(value, HashSHA256, SHA256.HashSizeInBits, EncodeBase64);
}
public static string ToSha512(this string value)
{
return ToHashed(value, HashSHA512, SHA512.HashSizeInBits, Encoding.UTF8, EncodeUTF8);
}
public static string ToSha512(this ReadOnlySpan<byte> value)
{
return ToHashed(value, HashSHA512, SHA512.HashSizeInBits, EncodeUTF8);
}
public static string ToSha256(this string value)
{
return ToHashed(value, HashSHA256, SHA256.HashSizeInBits, Encoding.UTF8, EncodeUTF8);
}
public static string ToSha256(this ReadOnlySpan<byte> value)
{
return ToHashed(value, HashSHA256, SHA256.HashSizeInBits, EncodeUTF8);
}
public static string ToMD5(this string value)
{
return ToHashed(value, HashMD5, MD5.HashSizeInBits, Encoding.UTF8, EncodeUTF8);
}
public static string ToMD5(this ReadOnlySpan<byte> value)
{
return ToHashed(value, HashMD5, MD5.HashSizeInBits, EncodeUTF8);
}
private static string ToHashed(this string value, HashMethod algorithm, int hashSize, Encoding encoding, EncodeMethod encode)
{
if (string.IsNullOrWhiteSpace(value))
{
return value;
}
string result;
var length = encoding.GetByteCount(value);
if (length > MaxStackSize)
{
var buffer = ArrayPool<byte>.Shared.Rent(length);
try
{
result = ConvertCore(algorithm, hashSize, value.AsSpan(), buffer, encode, encoding);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
else
{
Span<byte> buffer = stackalloc byte[length];
result = ConvertCore(algorithm, hashSize, value.AsSpan(), buffer, encode, encoding);
}
static string ConvertCore(HashMethod algorithm, int hashSize, ReadOnlySpan<char> source, Span<byte> destination, EncodeMethod encode, Encoding encoding)
{
var written = encoding.GetBytes(source, destination);
return ToHashed(destination[..written], algorithm, hashSize, encode);
}
return result;
}
private static string ToHashed(ReadOnlySpan<byte> bytes, HashMethod algorithm, int hashSize, EncodeMethod encode)
{
if (bytes.Length == 0)
{
return string.Empty;
}
string result;
var length = hashSize / 8;
if (length > MaxStackSize)
{
var buffer = ArrayPool<byte>.Shared.Rent(length);
try
{
result = ConvertCore(algorithm, bytes, buffer, encode);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
else
{
Span<byte> buffer = stackalloc byte[length];
return ConvertCore(algorithm, bytes, buffer, encode);
}
return result;
static string ConvertCore(HashMethod algorithm, ReadOnlySpan<byte> source, Span<byte> destination, EncodeMethod encode)
{
var written = algorithm(source, destination);
return encode(destination[..written]);
}
}
public static void AppendString(this IncrementalHash hash, string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return;
}
var length = Encoding.Default.GetByteCount(value);
if (length > MaxStackSize)
{
var buffer = ArrayPool<byte>.Shared.Rent(length);
try
{
ConvertCore(hash, value, buffer);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
else
{
Span<byte> buffer = stackalloc byte[length];
ConvertCore(hash, value, buffer);
}
static void ConvertCore(IncrementalHash hash, string source, Span<byte> destination)
{
var written = Encoding.Default.GetBytes(source, destination);
hash.AppendData(destination[..written]);
}
}
public static void AppendLong(this IncrementalHash hash, long version)
{
if (version == 0)
{
return;
}
Span<byte> buffer = stackalloc byte[sizeof(long)];
if (BitConverter.TryWriteBytes(buffer, version))
{
hash.AppendData(buffer);
}
}
public static string GetHexStringAndReset(this IncrementalHash hash)
{
Span<byte> buffer = stackalloc byte[hash.HashLengthInBytes];
hash.GetHashAndReset(buffer);
return ToHexString(buffer);
}
public static string ToHexString(this ReadOnlySpan<byte> data)
{
string result;
var length = data.Length * 2;
if (length > MaxStackSize)
{
var buffer = ArrayPool<char>.Shared.Rent(length);
try
{
result = ConvertCore(data, buffer);
}
finally
{
ArrayPool<char>.Shared.Return(buffer);
}
}
else
{
Span<char> buffer = stackalloc char[length];
result = ConvertCore(data, buffer);
}
return result;
static string ConvertCore(ReadOnlySpan<byte> data, Span<char> hexBuffer)
{
unchecked
{
int n = 0;
for (int i = 0; i < data.Length; i++)
{
byte b = data[i];
byte b1 = (byte)(b >> 4);
byte b2 = (byte)(b & 0xF);
hexBuffer[n++] = (b1 < 10) ? (char)('0' + b1) : (char)('A' + (b1 - 10));
hexBuffer[n++] = (b2 < 10) ? (char)('0' + b2) : (char)('A' + (b2 - 10));
}
}
return new string(hexBuffer);
}
}
}

12
backend/src/Squidex.Infrastructure/Json/IJsonSerializer.cs

@ -9,6 +9,10 @@ namespace Squidex.Infrastructure.Json;
public interface IJsonSerializer
{
byte[] SerializeToBytes<T>(T value, bool indented = false);
byte[] SerializeToBytes(object? value, Type type, bool indented = false);
string Serialize<T>(T value, bool indented = false);
string Serialize(object? value, Type type, bool indented = false);
@ -17,13 +21,13 @@ public interface IJsonSerializer
void Serialize(object? value, Type type, Stream stream, bool indented = false);
T Deserialize<T>(string value, Type? actualType = null);
T Deserialize<T>(Stream stream, Type? actualType = null);
Task SerializeAsync<T>(T value, Stream stream, bool indented = false,
CancellationToken ct = default);
Task SerializeAsync(object? value, Type type, Stream stream, bool indented = false,
CancellationToken ct = default);
T Deserialize<T>(string value, Type? actualType = null);
T Deserialize<T>(Stream stream, Type? actualType = null);
}

2
backend/src/Squidex.Infrastructure/Json/Objects/JsonValueType.cs

@ -9,9 +9,9 @@ namespace Squidex.Infrastructure.Json.Objects;
public enum JsonValueType
{
Null,
Array,
Boolean,
Null,
Number,
Object,
String

20
backend/src/Squidex.Infrastructure/Json/System/SystemJsonSerializer.cs

@ -74,6 +74,26 @@ public sealed class SystemJsonSerializer : IJsonSerializer
}
}
public byte[] SerializeToBytes<T>(T value, bool indented = false)
{
return SerializeToBytes(value, typeof(T), indented);
}
public byte[] SerializeToBytes(object? value, Type type, bool indented = false)
{
try
{
var options = indented ? optionsIndented : optionsNormal;
return JsonSerializer.SerializeToUtf8Bytes(value, type, options);
}
catch (SystemJsonException ex)
{
ThrowHelper.JsonException(ex.Message, ex);
return default!;
}
}
public void Serialize<T>(T value, Stream stream, bool indented = false)
{
Serialize(value, typeof(T), stream, indented);

59
backend/src/Squidex.Infrastructure/RandomHash.cs

@ -15,7 +15,8 @@ public static class RandomHash
public static string New()
{
return Guid.NewGuid()
.ToString().ToSha256Base64()
.ToString()
.ToSha256Base64()
.ToLowerInvariant()
.Replace('+', 'x')
.Replace('=', 'x')
@ -26,60 +27,4 @@ public static class RandomHash
{
return Guid.NewGuid().ToString().Replace("-", string.Empty, StringComparison.Ordinal);
}
public static string ToSha256Base64(this string value)
{
return ToSha256Base64(Encoding.UTF8.GetBytes(value));
}
public static string ToSha256Base64(this byte[] bytes)
{
var hashBytes = SHA256.HashData(bytes);
var result = Convert.ToBase64String(hashBytes);
return result;
}
public static string ToSha256(this string value)
{
return value.ToHashed(SHA256.Create());
}
public static string ToSha512(this string value)
{
return value.ToHashed(SHA512.Create());
}
public static string ToSha256(this byte[] bytes)
{
return bytes.ToHashed(SHA256.Create());
}
public static string ToMD5(this string value)
{
return value.ToHashed(MD5.Create());
}
public static string ToMD5(this byte[] bytes)
{
return bytes.ToHashed(MD5.Create());
}
public static string ToHashed(this string value, HashAlgorithm algorithm)
{
return Encoding.UTF8.GetBytes(value).ToHashed(algorithm);
}
public static string ToHashed(this byte[] bytes, HashAlgorithm algorithm)
{
using (algorithm)
{
var bytesHash = algorithm.ComputeHash(bytes);
var result = Encoding.UTF8.GetString(bytesHash);
return result;
}
}
}

119
backend/src/Squidex.Infrastructure/Reflection/Internal/PropertyAccessor.cs

@ -6,77 +6,98 @@
// ==========================================================================
using System.Reflection;
using System.Reflection.Emit;
namespace Squidex.Infrastructure.Reflection.Internal;
public sealed class PropertyAccessor
public static class PropertyAccessor
{
private interface IPropertyAccessor
{
object? Get(object target);
public delegate TValue Getter<TSource, TValue>(TSource source);
void Set(object target, object? value);
}
public delegate void Setter<TSource, TValue>(TSource source, TValue value);
private sealed class PropertyWrapper<TObject, TValue> : IPropertyAccessor
public static Getter<TSource, TValue> CreateGetter<TSource, TValue>(PropertyInfo propertyInfo)
{
private readonly Func<TObject, TValue> getMethod;
private readonly Action<TObject, TValue> setMethod;
public PropertyWrapper(PropertyInfo propertyInfo)
if (!propertyInfo.CanRead)
{
if (propertyInfo.CanRead)
{
getMethod = (Func<TObject, TValue>)propertyInfo.GetGetMethod(true)!.CreateDelegate(typeof(Func<TObject, TValue>));
}
else
{
getMethod = x => throw new NotSupportedException();
}
if (propertyInfo.CanWrite)
{
setMethod = (Action<TObject, TValue>)propertyInfo.GetSetMethod(true)!.CreateDelegate(typeof(Action<TObject, TValue>));
}
else
{
setMethod = (x, y) => throw new NotSupportedException();
}
return x => throw new NotSupportedException();
}
public object? Get(object source)
var bakingField =
propertyInfo.DeclaringType!.GetField($"<{propertyInfo.Name}>k__BackingField",
BindingFlags.NonPublic |
BindingFlags.Instance);
var propertyGetMethod = propertyInfo.GetGetMethod()!;
var getMethod = new DynamicMethod(propertyGetMethod.Name, typeof(TValue), new[] { typeof(TSource) }, true);
var getGenerator = getMethod.GetILGenerator();
// Load this to stack.
getGenerator.Emit(OpCodes.Ldarg_0);
if (bakingField != null && !propertyGetMethod.IsVirtual)
{
return getMethod((TObject)source);
// Get field directly.
getGenerator.Emit(OpCodes.Ldfld, bakingField);
}
public void Set(object source, object? value)
else if (propertyGetMethod.IsVirtual)
{
setMethod((TObject)source, (TValue)value!);
// Call the virtual property.
getGenerator.Emit(OpCodes.Callvirt, propertyGetMethod);
}
else
{
// Call the non virtual property.
getGenerator.Emit(OpCodes.Call, propertyGetMethod);
}
}
private readonly IPropertyAccessor internalAccessor;
getGenerator.Emit(OpCodes.Ret);
public PropertyAccessor(PropertyInfo propertyInfo)
return getMethod.CreateDelegate<Getter<TSource, TValue>>();
}
public static Setter<TSource, TValue> CreateSetter<TSource, TValue>(PropertyInfo propertyInfo)
{
Guard.NotNull(propertyInfo);
if (!propertyInfo.CanWrite)
{
return (x, y) => throw new NotSupportedException();
}
var type = typeof(PropertyWrapper<,>).MakeGenericType(propertyInfo.DeclaringType!, propertyInfo.PropertyType);
var bakingField =
propertyInfo.DeclaringType!.GetField($"<{propertyInfo.Name}>k__BackingField",
BindingFlags.NonPublic |
BindingFlags.Instance);
internalAccessor = (IPropertyAccessor)Activator.CreateInstance(type, propertyInfo)!;
}
var propertySetMethod = propertyInfo.GetSetMethod()!;
public object? Get(object target)
{
Guard.NotNull(target);
var setMethod = new DynamicMethod(propertySetMethod.Name, null, new[] { typeof(TSource), typeof(TValue) }, true);
var setGenerator = setMethod.GetILGenerator();
return internalAccessor.Get(target);
}
// Load this to stack.
setGenerator.Emit(OpCodes.Ldarg_0);
public void Set(object target, object? value)
{
Guard.NotNull(target);
// Load argument to stack.
setGenerator.Emit(OpCodes.Ldarg_1);
if (bakingField != null && !propertySetMethod.IsVirtual)
{
// Set the baking field directly.
setGenerator.Emit(OpCodes.Stfld, bakingField);
}
else if (propertySetMethod.IsVirtual)
{
// Call the virtual property.
setGenerator.Emit(OpCodes.Callvirt, propertySetMethod);
}
else
{
// Call the non virtual property.
setGenerator.Emit(OpCodes.Call, propertySetMethod);
}
setGenerator.Emit(OpCodes.Ret);
internalAccessor.Set(target, value);
return setMethod.CreateDelegate<Setter<TSource, TValue>>();
}
}

297
backend/src/Squidex.Infrastructure/Reflection/SimpleMapper.cs

@ -7,6 +7,7 @@
using System.ComponentModel;
using System.Globalization;
using System.Reflection;
using Squidex.Infrastructure.Reflection.Internal;
#pragma warning disable RECS0108 // Warns about static fields in generic types
@ -15,48 +16,135 @@ namespace Squidex.Infrastructure.Reflection;
public static class SimpleMapper
{
private readonly record struct MappingContext
internal readonly record struct MappingContext
{
required public CultureInfo Culture { get; init; }
required public bool NullableAsOptional { get; init; }
}
private sealed class StringConversionPropertyMapper : PropertyMapper
internal interface IPropertyMapper<TSource, TTarget>
{
public StringConversionPropertyMapper(
PropertyAccessor sourceAccessor,
PropertyAccessor targetAccessor)
: base(sourceAccessor, targetAccessor)
void MapProperty(TSource source, TTarget target, ref MappingContext context);
}
private static class SimplePropertyMapper
{
private static readonly MethodInfo CreateMethod =
typeof(SimplePropertyMapper)
.GetMethod(nameof(CreateCore),
BindingFlags.Static |
BindingFlags.NonPublic)!;
public static IPropertyMapper<TSource, TTarget> Create<TSource, TTarget>(Type valueType, PropertyInfo sourceProperty, PropertyInfo targetProperty)
{
var method = CreateMethod.MakeGenericMethod(typeof(TSource), typeof(TTarget), valueType);
return (IPropertyMapper<TSource, TTarget>)method.Invoke(null, new object?[] { sourceProperty, targetProperty })!;
}
public override void MapProperty(object source, object target, ref MappingContext context)
private static IPropertyMapper<TSource, TTarget> CreateCore<TSource, TTarget, TValue>(PropertyInfo sourceProperty, PropertyInfo targetProperty)
{
var value = GetValue(source);
return new SimplePropertyMapper<TSource, TTarget, TValue>(sourceProperty, targetProperty);
}
}
private sealed class SimplePropertyMapper<TSource, TTarget, TValue> : IPropertyMapper<TSource, TTarget>
{
private readonly PropertyAccessor.Getter<TSource, TValue> getter;
private readonly PropertyAccessor.Setter<TTarget, TValue> setter;
SetValue(target, value?.ToString());
public SimplePropertyMapper(PropertyInfo sourceProperty, PropertyInfo targetProperty)
{
getter = PropertyAccessor.CreateGetter<TSource, TValue>(sourceProperty);
setter = PropertyAccessor.CreateSetter<TTarget, TValue>(targetProperty);
}
public void MapProperty(TSource source, TTarget target, ref MappingContext context)
{
var value = getter(source);
setter(target, value);
}
}
private sealed class NullablePropertyMapper : PropertyMapper
private static class StringConversionPropertyMapper
{
private readonly object? defaultValue;
private static readonly MethodInfo CreateMethod =
typeof(StringConversionPropertyMapper)
.GetMethod(nameof(CreateCore),
BindingFlags.Static |
BindingFlags.NonPublic)!;
public NullablePropertyMapper(
PropertyAccessor sourceAccessor,
PropertyAccessor targetAccessor,
object? defaultValue)
: base(sourceAccessor, targetAccessor)
public static IPropertyMapper<TSource, TTarget> Create<TSource, TTarget>(Type valueType, PropertyInfo sourceProperty, PropertyInfo targetProperty)
{
this.defaultValue = defaultValue;
var method = CreateMethod.MakeGenericMethod(typeof(TSource), typeof(TTarget), valueType);
return (IPropertyMapper<TSource, TTarget>)method.Invoke(null, new object?[] { sourceProperty, targetProperty })!;
}
public override void MapProperty(object source, object target, ref MappingContext context)
private static IPropertyMapper<TSource, TTarget> CreateCore<TSource, TTarget, TValue>(PropertyInfo sourceProperty, PropertyInfo targetProperty)
{
var value = GetValue(source);
return new StringConversionPropertyMapper<TSource, TTarget, TValue>(sourceProperty, targetProperty);
}
}
if (value == null)
private sealed class StringConversionPropertyMapper<TSource, TTarget, TValue> : IPropertyMapper<TSource, TTarget>
{
private readonly PropertyAccessor.Getter<TSource, TValue> getter;
private readonly PropertyAccessor.Setter<TTarget, string> setter;
public StringConversionPropertyMapper(PropertyInfo sourceProperty, PropertyInfo targetProperty)
{
getter = PropertyAccessor.CreateGetter<TSource, TValue>(sourceProperty);
setter = PropertyAccessor.CreateSetter<TTarget, string>(targetProperty);
}
public void MapProperty(TSource source, TTarget target, ref MappingContext context)
{
var value = getter(source);
setter(target, value?.ToString()!);
}
}
private static class NullablePropertyMapper
{
private static readonly MethodInfo CreateMethod =
typeof(NullablePropertyMapper)
.GetMethod(nameof(CreateCore),
BindingFlags.Static |
BindingFlags.NonPublic)!;
public static IPropertyMapper<TSource, TTarget> Create<TSource, TTarget>(Type valueType, PropertyInfo sourceProperty, PropertyInfo targetProperty)
{
var method = CreateMethod.MakeGenericMethod(typeof(TSource), typeof(TTarget), valueType);
return (IPropertyMapper<TSource, TTarget>)method.Invoke(null, new object?[] { sourceProperty, targetProperty })!;
}
private static IPropertyMapper<TSource, TTarget> CreateCore<TSource, TTarget, TValue>(PropertyInfo sourceProperty, PropertyInfo targetProperty) where TValue : struct
{
return new NullablePropertyMapper<TSource, TTarget, TValue>(sourceProperty, targetProperty);
}
}
private sealed class NullablePropertyMapper<TSource, TTarget, TValue> : IPropertyMapper<TSource, TTarget> where TValue : struct
{
private readonly PropertyAccessor.Getter<TSource, TValue?> getter;
private readonly PropertyAccessor.Setter<TTarget, TValue> setter;
public NullablePropertyMapper(PropertyInfo sourceProperty, PropertyInfo targetProperty)
{
getter = PropertyAccessor.CreateGetter<TSource, TValue?>(sourceProperty);
setter = PropertyAccessor.CreateSetter<TTarget, TValue>(targetProperty);
}
public void MapProperty(TSource source, TTarget target, ref MappingContext context)
{
var value = getter(source);
if (value is null)
{
if (context.NullableAsOptional)
{
@ -64,65 +152,49 @@ public static class SimpleMapper
}
else
{
value = defaultValue;
value = default(TValue);
}
}
SetValue(target, value);
setter(target, value.Value);
}
}
private sealed class ConversionPropertyMapper : PropertyMapper
private static class ConversionPropertyMapper
{
private readonly Type targetType;
private static readonly MethodInfo CreateMethod =
typeof(ConversionPropertyMapper)
.GetMethod(nameof(CreateCore),
BindingFlags.Static |
BindingFlags.NonPublic)!;
public ConversionPropertyMapper(
PropertyAccessor sourceAccessor,
PropertyAccessor targetAccessor,
Type targetType)
: base(sourceAccessor, targetAccessor)
public static IPropertyMapper<TSource, TTarget> Create<TSource, TTarget>(Type sourceType, PropertyInfo sourceProperty, Type targetType, PropertyInfo targetProperty)
{
this.targetType = targetType;
var method = CreateMethod.MakeGenericMethod(typeof(TSource), typeof(TTarget), sourceType, targetType);
return (IPropertyMapper<TSource, TTarget>)method.Invoke(null, new object?[] { sourceProperty, targetProperty })!;
}
public override void MapProperty(object source, object target, ref MappingContext context)
private static IPropertyMapper<TSource, TTarget> CreateCore<TSource, TTarget, TSourceValue, TTargetValue>(PropertyInfo sourceProperty, PropertyInfo targetProperty)
{
var value = GetValue(source);
if (value == null)
{
return;
}
try
{
var converted = Convert.ChangeType(value, targetType, context.Culture);
SetValue(target, converted);
}
catch
{
return;
}
return new ConversionPropertyMapper<TSource, TTarget, TSourceValue, TTargetValue>(sourceProperty, targetProperty);
}
}
private sealed class TypeConverterPropertyMapper : PropertyMapper
private sealed class ConversionPropertyMapper<TSource, TTarget, TSourceValue, TTargetValue> : IPropertyMapper<TSource, TTarget>
{
private readonly TypeConverter converter;
private readonly PropertyAccessor.Getter<TSource, TSourceValue> getter;
private readonly PropertyAccessor.Setter<TTarget, TTargetValue> setter;
public TypeConverterPropertyMapper(
PropertyAccessor sourceAccessor,
PropertyAccessor targetAccessor,
TypeConverter converter)
: base(sourceAccessor, targetAccessor)
public ConversionPropertyMapper(PropertyInfo sourceProperty, PropertyInfo targetProperty)
{
this.converter = converter;
getter = PropertyAccessor.CreateGetter<TSource, TSourceValue>(sourceProperty);
setter = PropertyAccessor.CreateSetter<TTarget, TTargetValue>(targetProperty);
}
public override void MapProperty(object source, object target, ref MappingContext context)
public void MapProperty(TSource source, TTarget target, ref MappingContext context)
{
var value = GetValue(source);
var value = (object?)getter(source);
if (value == null)
{
@ -131,9 +203,9 @@ public static class SimpleMapper
try
{
var converted = converter.ConvertFrom(null, context.Culture, value);
var converted = (TTargetValue)Convert.ChangeType(value, typeof(TTargetValue), context.Culture);
SetValue(target, converted);
setter(target, converted);
}
catch
{
@ -142,38 +214,66 @@ public static class SimpleMapper
}
}
private class PropertyMapper
private static class TypeConverterPropertyMapper
{
private readonly PropertyAccessor sourceAccessor;
private readonly PropertyAccessor targetAccessor;
private static readonly MethodInfo CreateMethod =
typeof(TypeConverterPropertyMapper)
.GetMethod(nameof(CreateCore),
BindingFlags.Static |
BindingFlags.NonPublic)!;
public PropertyMapper(PropertyAccessor sourceAccessor, PropertyAccessor targetAccessor)
public static IPropertyMapper<TSource, TTarget> Create<TSource, TTarget>(Type sourceType, PropertyInfo sourceProperty, Type targetType, PropertyInfo targetProperty, TypeConverter typeConverter)
{
this.sourceAccessor = sourceAccessor;
this.targetAccessor = targetAccessor;
var method = CreateMethod.MakeGenericMethod(typeof(TSource), typeof(TTarget), sourceType, targetType);
return (IPropertyMapper<TSource, TTarget>)method.Invoke(null, new object?[] { sourceProperty, targetProperty, typeConverter })!;
}
public virtual void MapProperty(object source, object target, ref MappingContext context)
private static IPropertyMapper<TSource, TTarget> CreateCore<TSource, TTarget, TSourceValue, TTargetValue>(PropertyInfo sourceProperty, PropertyInfo targetProperty, TypeConverter typeConverter)
{
var value = GetValue(source);
SetValue(target, value);
return new TypeConverterPropertyMapper<TSource, TTarget, TSourceValue, TTargetValue>(sourceProperty, targetProperty, typeConverter);
}
}
private sealed class TypeConverterPropertyMapper<TSource, TTarget, TSourceType, TTargetType> : IPropertyMapper<TSource, TTarget>
{
private readonly PropertyAccessor.Getter<TSource, TSourceType> getter;
private readonly PropertyAccessor.Setter<TTarget, TTargetType> setter;
private readonly TypeConverter typeConverter;
protected void SetValue(object destination, object? value)
public TypeConverterPropertyMapper(PropertyInfo sourceProperty, PropertyInfo targetProperty, TypeConverter typeConverter)
{
targetAccessor.Set(destination, value);
getter = PropertyAccessor.CreateGetter<TSource, TSourceType>(sourceProperty);
setter = PropertyAccessor.CreateSetter<TTarget, TTargetType>(targetProperty);
this.typeConverter = typeConverter;
}
protected object? GetValue(object source)
public void MapProperty(TSource source, TTarget target, ref MappingContext context)
{
return sourceAccessor.Get(source);
var value = (object?)getter(source);
if (value == null)
{
return;
}
try
{
var converted = typeConverter.ConvertFrom(null, context.Culture, value);
setter(target, (TTargetType)converted!);
}
catch
{
return;
}
}
}
private static class ClassMapper<TSource, TTarget> where TSource : class where TTarget : class
{
private static readonly List<PropertyMapper> Mappers = new List<PropertyMapper>();
private static readonly List<IPropertyMapper<TSource, TTarget>> Mappers = new List<IPropertyMapper<TSource, TTarget>>();
static ClassMapper()
{
@ -201,22 +301,24 @@ public static class SimpleMapper
if (sourceType == targetType)
{
Mappers.Add(new PropertyMapper(
new PropertyAccessor(sourceProperty),
new PropertyAccessor(targetProperty)));
Mappers.Add(SimplePropertyMapper.Create<TSource, TTarget>(
sourceType,
sourceProperty,
targetProperty));
}
else if (targetType == typeof(string))
{
Mappers.Add(new StringConversionPropertyMapper(
new PropertyAccessor(sourceProperty),
new PropertyAccessor(targetProperty)));
Mappers.Add(StringConversionPropertyMapper.Create<TSource, TTarget>(
sourceType,
sourceProperty,
targetProperty));
}
else if (IsNullableOf(sourceType, targetType))
{
Mappers.Add(new NullablePropertyMapper(
new PropertyAccessor(sourceProperty),
new PropertyAccessor(targetProperty),
Activator.CreateInstance(targetType)));
Mappers.Add(NullablePropertyMapper.Create<TSource, TTarget>(
targetType,
sourceProperty,
targetProperty));
}
else
{
@ -224,17 +326,20 @@ public static class SimpleMapper
if (converter.CanConvertFrom(sourceType))
{
Mappers.Add(new TypeConverterPropertyMapper(
new PropertyAccessor(sourceProperty),
new PropertyAccessor(targetProperty),
Mappers.Add(TypeConverterPropertyMapper.Create<TSource, TTarget>(
sourceType,
sourceProperty,
targetType,
targetProperty,
converter));
}
else if (sourceType.Implements<IConvertible>() || targetType.Implements<IConvertible>())
{
Mappers.Add(new ConversionPropertyMapper(
new PropertyAccessor(sourceProperty),
new PropertyAccessor(targetProperty),
targetType));
Mappers.Add(ConversionPropertyMapper.Create<TSource, TTarget>(
sourceType,
sourceProperty,
targetType,
targetProperty));
}
}
}
@ -249,11 +354,11 @@ public static class SimpleMapper
public static TTarget MapClass(TSource source, TTarget destination, ref MappingContext context)
{
for (var i = 0; i < Mappers.Count; i++)
{
var mapper = Mappers[i];
var count = Mappers.Count;
mapper.MapProperty(source, destination, ref context);
for (var i = 0; i < count; i++)
{
Mappers[i].MapProperty(source, destination, ref context);
}
return destination;

22
backend/src/Squidex.Infrastructure/StringExtensions.cs

@ -45,28 +45,6 @@ public static class StringExtensions
return !string.IsNullOrWhiteSpace(value) ? value.Trim() : fallback;
}
public static string ToHexString(this byte[] data)
{
unchecked
{
var hex = new char[data.Length * 2];
int n = 0;
for (int i = 0; i < data.Length; i++)
{
byte b = data[i];
byte b1 = (byte)(b >> 4);
byte b2 = (byte)(b & 0xF);
hex[n++] = (b1 < 10) ? (char)('0' + b1) : (char)('A' + (b1 - 10));
hex[n++] = (b2 < 10) ? (char)('0' + b2) : (char)('A' + (b2 - 10));
}
return new string(hex);
}
}
public static string JoinNonEmpty(string separator, params string?[] parts)
{
Guard.NotNull(separator);

11
backend/src/Squidex.Web/ETagExtensions.cs

@ -40,18 +40,15 @@ public static class ETagExtensions
{
using (var hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA256))
{
hasher.AppendData(BitConverter.GetBytes(total));
hasher.AppendLong(total);
foreach (var item in entities)
{
hasher.AppendData(Encoding.Default.GetBytes(item.UniqueId.ToString()));
hasher.AppendData(BitConverter.GetBytes(item.Version));
hasher.AppendString(item.UniqueId.ToString());
hasher.AppendLong(item.Version);
}
var cacheBuffer = hasher.GetHashAndReset();
var cacheString = BitConverter.ToString(cacheBuffer).Replace("-", string.Empty, StringComparison.Ordinal).ToUpperInvariant();
return cacheString;
return hasher.GetHexStringAndReset();
}
}

18
backend/src/Squidex.Web/Pipeline/CachingManager.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Buffers;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
@ -23,6 +24,7 @@ namespace Squidex.Web.Pipeline;
public sealed class CachingManager : IRequestCache
{
public const string SurrogateKeySizeHeader = "X-SurrogateKeys";
private const int MaxStackSize = 256;
private const int MaxAllowedKeysSize = 20000;
private readonly ObjectPool<StringBuilder> stringBuilderPool;
private readonly CachingOptions cachingOptions;
@ -60,10 +62,13 @@ public sealed class CachingManager : IRequestCache
slimLock.EnterWriteLock();
try
{
keys.Add(key);
if (!keys.Add(key))
{
return;
}
hasher.AppendData(Encoding.Default.GetBytes(key));
hasher.AppendData(BitConverter.GetBytes(version));
hasher.AppendString(key);
hasher.AppendLong(version);
hasDependency = true;
}
@ -85,7 +90,7 @@ public sealed class CachingManager : IRequestCache
slimLock.EnterWriteLock();
try
{
hasher.AppendData(Encoding.Default.GetBytes(formatted));
hasher.AppendString(formatted);
hasDependency = true;
}
@ -113,10 +118,7 @@ public sealed class CachingManager : IRequestCache
{
using (Telemetry.Activities.StartActivity("CalculateEtag"))
{
var cacheBuffer = hasher.GetHashAndReset();
var cacheString = cacheBuffer.ToHexString();
response.Headers.Add(HeaderNames.ETag, cacheString);
response.Headers.Add(HeaderNames.ETag, hasher.GetHexStringAndReset());
}
}

4
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/ClientDto.cs

@ -50,9 +50,7 @@ public sealed class ClientDto : Resource
public static ClientDto FromClient(string id, AppClient client)
{
var result = SimpleMapper.Map(client, new ClientDto { Id = id });
return result;
return SimpleMapper.Map(client, new ClientDto { Id = id });
}
public ClientDto CreateLinks(Resources resources)

182
backend/tests/Squidex.Infrastructure.Tests/HashExtensionsTests.cs

@ -0,0 +1,182 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Security.Cryptography;
using System.Text;
namespace Squidex.Infrastructure;
public class HashExtensionsTests
{
public static IEnumerable<object[]> InputLengths()
{
yield return new object[] { 1 };
yield return new object[] { 20 };
yield return new object[] { 50 };
yield return new object[] { 500 };
}
[Fact]
public void Should_calculate_hex_code_from_empty_array()
{
var source = Array.Empty<byte>();
var actual = ((ReadOnlySpan<byte>)source.AsSpan()).ToHexString();
Assert.Equal(string.Empty, actual);
}
[Fact]
public void Should_calculate_hex_code_from_byte_array()
{
var source = new byte[] { 0x00, 0x01, 0xFF, 0x1A, 0x2B, 0x3C };
var actual = ((ReadOnlySpan<byte>)source.AsSpan()).ToHexString();
Assert.Equal("0001FF1A2B3C", actual);
}
[Theory]
[MemberData(nameof(InputLengths))]
public void Should_hash_md5(int length)
{
AssertHash(HashExtensions.ToMD5, length);
}
[Theory]
[MemberData(nameof(InputLengths))]
public void Should_hash_md5_from_bytes(int length)
{
AssertHashFromBytes(HashExtensions.ToMD5, length);
}
[Theory]
[MemberData(nameof(InputLengths))]
public void Should_hash_sha256(int length)
{
AssertHash(HashExtensions.ToSha256, length);
}
[Theory]
[MemberData(nameof(InputLengths))]
public void Should_hash_sha256_from_bytes(int length)
{
AssertHashFromBytes(HashExtensions.ToSha256, length);
}
[Theory]
[MemberData(nameof(InputLengths))]
public void Should_hash_sha512(int length)
{
AssertHash(HashExtensions.ToSha512, length);
}
[Theory]
[MemberData(nameof(InputLengths))]
public void Should_hash_sha512_from_bytes(int length)
{
AssertHashFromBytes(HashExtensions.ToSha512, length);
}
[Theory]
[MemberData(nameof(InputLengths))]
public void Should_hash_to_sha256_base64(int length)
{
AssertHash(HashExtensions.ToSha256Base64, length);
}
[Theory]
[MemberData(nameof(InputLengths))]
public void Should_hash_sha256_base64_from_bytes(int length)
{
AssertHashFromBytes(HashExtensions.ToSha256Base64, length);
}
[Fact]
public void Should_convert_same_hash_as_webhook_utils()
{
var input = $"{Guid.NewGuid()}{new string('0', 100)}{Guid.NewGuid()}";
var hash1 = WebhookHash(input);
var hash2 = input.ToSha256Base64();
Assert.Equal(hash1, hash2);
}
private delegate string HashFromBytes(ReadOnlySpan<byte> source);
private static void AssertHashFromBytes(HashFromBytes hasher, int length)
{
var input1 = CreateBuffer(length, 0);
var input2 = CreateBuffer(length, 42);
var hash1_a = hasher(input1);
var hash1_b = hasher(input1);
var hash2 = hasher(input2);
Assert.NotNull(hash1_a);
Assert.NotNull(hash1_a);
Assert.NotNull(hash2);
Assert.NotEmpty(hash1_a);
Assert.NotEmpty(hash1_b);
Assert.NotEmpty(hash2);
Assert.Equal(hash1_a, hash1_b);
Assert.NotEqual(hash1_a, hash2);
}
private static void AssertHash(Func<string, string> hasher, int length)
{
var input1 = new string('a', length);
var input2 = new string('b', length);
var hash1_a = hasher(input1);
var hash1_b = hasher(input1);
var hash2 = hasher(input2);
Assert.NotNull(hash1_a);
Assert.NotNull(hash1_a);
Assert.NotNull(hash2);
Assert.NotEmpty(hash1_a);
Assert.NotEmpty(hash1_b);
Assert.NotEmpty(hash2);
Assert.Equal(hash1_a, hash1_b);
Assert.NotEqual(hash1_a, hash2);
}
private static byte[] CreateBuffer(int length, int offset)
{
var input = new byte[length];
for (var i = 0; i < length; i++)
{
input[i] = (byte)(i + offset);
}
return input;
}
private static string WebhookHash(string value)
{
using (var sha = SHA256.Create())
{
var bytesArray = Encoding.UTF8.GetBytes(value);
var bytesHash = sha.ComputeHash(bytesArray);
var result = Convert.ToBase64String(bytesHash);
return result;
}
}
}

98
backend/tests/Squidex.Infrastructure.Tests/Reflection/PropertiesTypeAccessorTests.cs

@ -1,98 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Reflection.Internal;
namespace Squidex.Infrastructure.Reflection;
public class PropertiesTypeAccessorTests
{
public class TestClass
{
private int target;
public int ReadWrite
{
get
{
return target;
}
set
{
target = value;
}
}
public int Read
{
get => target;
}
public int Write
{
set => target = value;
}
}
private readonly TestClass target = new TestClass();
[Fact]
public void Should_set_read_write_property()
{
var sut = new PropertyAccessor(typeof(TestClass).GetProperty("ReadWrite")!);
sut.Set(target, 123);
Assert.Equal(123, target.Read);
}
[Fact]
public void Should_set_write_property()
{
var accessor = new PropertyAccessor(typeof(TestClass).GetProperty("Write")!);
accessor.Set(target, 123);
Assert.Equal(123, target.Read);
}
[Fact]
public void Should_throw_exception_if_setting_readonly()
{
var sut = new PropertyAccessor(typeof(TestClass).GetProperty("Read")!);
Assert.Throws<NotSupportedException>(() => sut.Set(target, 123));
}
[Fact]
public void Should_get_read_write_property()
{
var sut = new PropertyAccessor(typeof(TestClass).GetProperty("ReadWrite")!);
target.Write = 123;
Assert.Equal(123, sut.Get(target));
}
[Fact]
public void Should_get_read_property()
{
var sut = new PropertyAccessor(typeof(TestClass).GetProperty("Read")!);
target.Write = 123;
Assert.Equal(123, sut.Get(target));
}
[Fact]
public void Should_throw_exception_if_getting_writeonly_property()
{
var sut = new PropertyAccessor(typeof(TestClass).GetProperty("Write")!);
Assert.Throws<NotSupportedException>(() => sut.Get(target));
}
}

153
backend/tests/Squidex.Infrastructure.Tests/Reflection/PropertyAccessorTests.cs

@ -0,0 +1,153 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Reflection.Internal;
namespace Squidex.Infrastructure.Reflection;
public class PropertyAccessorTests
{
public class TestClass
{
private int readWrite;
private int readWriteVirtual;
public int ReadWrite
{
get
{
return readWrite;
}
set
{
readWrite = value;
}
}
public virtual int ReadWriteVirtual
{
get
{
return readWriteVirtual;
}
set
{
readWriteVirtual = value;
}
}
public int ReadWriteAuto { get; set; }
public int Read
{
get => readWrite;
}
public int Write
{
set => readWrite = value;
}
}
private readonly TestClass target = new TestClass();
[Fact]
public void Should_set_read_write_property()
{
var sut = PropertyAccessor.CreateSetter<TestClass, int>(typeof(TestClass).GetProperty("ReadWrite")!);
sut(target, 123);
Assert.Equal(123, target.Read);
}
[Fact]
public void Should_set_virtual_read_write_property()
{
var sut = PropertyAccessor.CreateSetter<TestClass, int>(typeof(TestClass).GetProperty("ReadWriteVirtual")!);
sut(target, 123);
Assert.Equal(123, target.ReadWriteVirtual);
}
[Fact]
public void Should_set_auto_implemented_read_write_property()
{
var sut = PropertyAccessor.CreateSetter<TestClass, int>(typeof(TestClass).GetProperty("ReadWriteAuto")!);
sut(target, 123);
Assert.Equal(123, target.ReadWriteAuto);
}
[Fact]
public void Should_set_write_property()
{
var sut = PropertyAccessor.CreateSetter<TestClass, int>(typeof(TestClass).GetProperty("Write")!);
sut(target, 123);
Assert.Equal(123, target.Read);
}
[Fact]
public void Should_throw_exception_if_setting_readonly()
{
var sut = PropertyAccessor.CreateSetter<TestClass, int>(typeof(TestClass).GetProperty("Read")!);
Assert.Throws<NotSupportedException>(() => sut(target, 123));
}
[Fact]
public void Should_get_read_write_property()
{
var sut = PropertyAccessor.CreateGetter<TestClass, int>(typeof(TestClass).GetProperty("ReadWrite")!);
target.Write = 123;
Assert.Equal(123, sut(target));
}
[Fact]
public void Should_get_virtual_read_write_property()
{
var sut = PropertyAccessor.CreateGetter<TestClass, int>(typeof(TestClass).GetProperty("ReadWriteVirtual")!);
target.ReadWriteVirtual = 123;
Assert.Equal(123, sut(target));
}
[Fact]
public void Should_get_auto_implemented_read_write_property()
{
var sut = PropertyAccessor.CreateGetter<TestClass, int>(typeof(TestClass).GetProperty("ReadWriteAuto")!);
target.ReadWriteAuto = 123;
Assert.Equal(123, sut(target));
}
[Fact]
public void Should_get_read_property()
{
var sut = PropertyAccessor.CreateGetter<TestClass, int>(typeof(TestClass).GetProperty("Read")!);
target.Write = 123;
Assert.Equal(123, sut(target));
}
[Fact]
public void Should_throw_exception_if_getting_writeonly_property()
{
var sut = PropertyAccessor.CreateGetter<TestClass, int>(typeof(TestClass).GetProperty("Write")!);
Assert.Throws<NotSupportedException>(() => sut(target));
}
}

16
backend/tests/Squidex.Infrastructure.Tests/StringExtensionsTests.cs

@ -78,22 +78,6 @@ public class StringExtensionsTests
Assert.Equal("Hello \\\"World\\\"", actual);
}
[Fact]
public void Should_calculate_hex_code_from_empty_array()
{
var actual = Array.Empty<byte>().ToHexString();
Assert.Equal(string.Empty, actual);
}
[Fact]
public void Should_calculate_hex_code_from_byte_array()
{
var actual = new byte[] { 0x00, 0x01, 0xFF, 0x1A, 0x2B, 0x3C }.ToHexString();
Assert.Equal("0001FF1A2B3C", actual);
}
[Theory]
[InlineData("", "")]
[InlineData(" ", "")]

Loading…
Cancel
Save