From 899b972b2ec72077c4116baf6fd3636b7599c0bc Mon Sep 17 00:00:00 2001 From: yv989c Date: Fri, 17 Mar 2023 00:16:28 -0400 Subject: [PATCH 01/29] feat: implements JsonSerializer --- .../Serializers/IJsonSerializer.cs | 6 + .../Serializers/JsonSerializer.cs | 221 ++++++++++++++++++ src/SharedProjectProperties.xml | 6 +- 3 files changed, 231 insertions(+), 2 deletions(-) create mode 100644 src/QueryableValues.SqlServer/Serializers/IJsonSerializer.cs create mode 100644 src/QueryableValues.SqlServer/Serializers/JsonSerializer.cs diff --git a/src/QueryableValues.SqlServer/Serializers/IJsonSerializer.cs b/src/QueryableValues.SqlServer/Serializers/IJsonSerializer.cs new file mode 100644 index 0000000..fe4ef74 --- /dev/null +++ b/src/QueryableValues.SqlServer/Serializers/IJsonSerializer.cs @@ -0,0 +1,6 @@ +namespace BlazarTech.QueryableValues.Serializers +{ + internal interface IJsonSerializer : ISerializer + { + } +} diff --git a/src/QueryableValues.SqlServer/Serializers/JsonSerializer.cs b/src/QueryableValues.SqlServer/Serializers/JsonSerializer.cs new file mode 100644 index 0000000..38f5d43 --- /dev/null +++ b/src/QueryableValues.SqlServer/Serializers/JsonSerializer.cs @@ -0,0 +1,221 @@ +using Microsoft.IO; +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json; + +namespace BlazarTech.QueryableValues.Serializers +{ + internal sealed class JsonSerializer : IJsonSerializer + { + private static readonly RecyclableMemoryStreamManager MemoryStreamManager = new RecyclableMemoryStreamManager(); + + private static string SerializePrivate(T values) + { + return System.Text.Json.JsonSerializer.Serialize(values); + } + + public string Serialize(IEnumerable values) + { + return SerializePrivate(values); + } + + public string Serialize(IEnumerable values) + { + return SerializePrivate(values); + } + + public string Serialize(IEnumerable values) + { + return SerializePrivate(values); + } + + public string Serialize(IEnumerable values) + { + return SerializePrivate(values); + } + + public string Serialize(IEnumerable values) + { + return SerializePrivate(values); + } + + public string Serialize(IEnumerable values) + { + return SerializePrivate(values); + } + + public string Serialize(IEnumerable values) + { + return SerializePrivate(values); + } + + public string Serialize(IEnumerable values) + { + return SerializePrivate(values); + } + + public string Serialize(IEnumerable values) + { + return SerializePrivate(values); + } + + public string Serialize(IEnumerable values) + { + return SerializePrivate(values); + } + + public string Serialize(IEnumerable values) + { + return SerializePrivate(values); + } + + public string Serialize(IEnumerable values) + { + return SerializePrivate(values); + } + + public string Serialize(IEnumerable values, IReadOnlyList propertyMappings) + where T : notnull + { + var properties = new PropertyWriter[propertyMappings.Count]; + + for (var i = 0; i < properties.Length; i++) + { + properties[i] = new PropertyWriter(propertyMappings[i]); + } + + void writeEntity(Utf8JsonWriter writer, T entity) + { + for (int i = 0; i < properties.Length; i++) + { + properties[i].WriteValue(writer, entity); + } + } + + static bool mustSkipValue(T v) => v is null; + + return GetJson(values, writeEntity, mustSkipValue); + + static string GetJson(IEnumerable values, Action writeValue, Func? mustSkipValue = null) + { + using var stream = MemoryStreamManager.GetStream(); + + using (var jsonWriter = new Utf8JsonWriter(stream)) + { + jsonWriter.WriteStartArray(); + + foreach (var value in values) + { + if (mustSkipValue?.Invoke(value) == true) + { + continue; + } + + jsonWriter.WriteStartObject(); + + writeValue(jsonWriter, value); + + jsonWriter.WriteEndObject(); + } + + jsonWriter.WriteEndArray(); + } + + var streamInt32Length = (int)stream.Length; +#if NETSTANDARD2_0 + return Encoding.UTF8.GetString(stream.GetBuffer(), 0, streamInt32Length); +#else + var streamBufferSpan = stream.GetBuffer().AsSpan(0, streamInt32Length); + return Encoding.UTF8.GetString(streamBufferSpan); +#endif + } + } + + private sealed class PropertyWriter + { + private static readonly Action WriteBool = (Utf8JsonWriter writer, bool value) => writer.WriteBooleanValue(value); + private static readonly Action WriteByte = (Utf8JsonWriter writer, byte value) => writer.WriteNumberValue(value); + private static readonly Action WriteInt16 = (Utf8JsonWriter writer, short value) => writer.WriteNumberValue(value); + private static readonly Action WriteInt32 = (Utf8JsonWriter writer, int value) => writer.WriteNumberValue(value); + private static readonly Action WriteInt64 = (Utf8JsonWriter writer, long value) => writer.WriteNumberValue(value); + private static readonly Action WriteDecimal = (Utf8JsonWriter writer, decimal value) => writer.WriteNumberValue(value); + private static readonly Action WriteSingle = (Utf8JsonWriter writer, float value) => writer.WriteNumberValue(value); + private static readonly Action WriteDouble = (Utf8JsonWriter writer, double value) => writer.WriteNumberValue(value); + + private static readonly Action WriteDateTime = (Utf8JsonWriter writer, DateTime value) => + { + if (value.Kind != DateTimeKind.Unspecified) + { + writer.WriteStringValue(DateTime.SpecifyKind(value, DateTimeKind.Unspecified)); + } + else + { + writer.WriteStringValue(value); + } + }; + + private static readonly Action WriteDateTimeOffset = (Utf8JsonWriter writer, DateTimeOffset value) => writer.WriteStringValue(value); + private static readonly Action WriteGuid = (Utf8JsonWriter writer, Guid value) => writer.WriteStringValue(value); + private static readonly Action WriteChar = (Utf8JsonWriter writer, char value) => writer.WriteStringValue(stackalloc[] { value }); + + private readonly string _targetName; + private readonly Action? _writeValue; + + public EntityPropertyMapping Mapping { get; } + + public PropertyWriter(EntityPropertyMapping mapping) + { + Mapping = mapping; + + _targetName = mapping.Target.Name; + + _writeValue = mapping.TypeName switch + { + EntityPropertyTypeName.Boolean => (writer, value) => WriteAttribute(writer, (bool?)value, WriteBool), + EntityPropertyTypeName.Byte => (writer, value) => WriteAttribute(writer, (byte?)value, WriteByte), + EntityPropertyTypeName.Int16 => (writer, value) => WriteAttribute(writer, (short?)value, WriteInt16), + EntityPropertyTypeName.Int32 => (writer, value) => WriteAttribute(writer, (int?)value, WriteInt32), + EntityPropertyTypeName.Int64 => (writer, value) => WriteAttribute(writer, (long?)value, WriteInt64), + EntityPropertyTypeName.Decimal => (writer, value) => WriteAttribute(writer, (decimal?)value, WriteDecimal), + EntityPropertyTypeName.Single => (writer, value) => WriteAttribute(writer, (float?)value, WriteSingle), + EntityPropertyTypeName.Double => (writer, value) => WriteAttribute(writer, (double?)value, WriteDouble), + EntityPropertyTypeName.DateTime => (writer, value) => WriteAttribute(writer, (DateTime?)value, WriteDateTime), + EntityPropertyTypeName.DateTimeOffset => (writer, value) => WriteAttribute(writer, (DateTimeOffset?)value, WriteDateTimeOffset), + EntityPropertyTypeName.Guid => (writer, value) => WriteAttribute(writer, (Guid?)value, WriteGuid), + EntityPropertyTypeName.Char => (writer, value) => WriteAttribute(writer, (char?)value, WriteChar), + EntityPropertyTypeName.String => (writer, value) => WriteStringAttribute(writer, (string?)value), + _ => throw new NotImplementedException(mapping.TypeName.ToString()), + }; + } + + private void WriteAttribute(Utf8JsonWriter writer, TValue? value, Action writeValue) + where TValue : struct + { + if (value.HasValue) + { + writer.WritePropertyName(_targetName); + writeValue(writer, value.Value); + } + } + + private void WriteStringAttribute(Utf8JsonWriter writer, string? value) + { + if (value != null) + { + writer.WritePropertyName(_targetName); + writer.WriteStringValue(value); + } + } + + public void WriteValue(Utf8JsonWriter writer, object entity) + { + if (_writeValue != null) + { + var value = Mapping.Source.GetValue(entity); + _writeValue.Invoke(writer, value); + } + } + } + } +} diff --git a/src/SharedProjectProperties.xml b/src/SharedProjectProperties.xml index 9dac96c..c961c9b 100644 --- a/src/SharedProjectProperties.xml +++ b/src/SharedProjectProperties.xml @@ -19,9 +19,11 @@ - + + + - + True \ From 90c3d86900c880ef3e4666cdc4ce2946854302b2 Mon Sep 17 00:00:00 2001 From: yv989c Date: Fri, 17 Mar 2023 00:54:06 -0400 Subject: [PATCH 02/29] refactor: JsonSerializer --- .../Serializers/JsonSerializer.cs | 10 +- src/SharedProjectProperties.xml | 106 +++++++++--------- 2 files changed, 58 insertions(+), 58 deletions(-) diff --git a/src/QueryableValues.SqlServer/Serializers/JsonSerializer.cs b/src/QueryableValues.SqlServer/Serializers/JsonSerializer.cs index 38f5d43..cb0824f 100644 --- a/src/QueryableValues.SqlServer/Serializers/JsonSerializer.cs +++ b/src/QueryableValues.SqlServer/Serializers/JsonSerializer.cs @@ -1,5 +1,6 @@ using Microsoft.IO; using System; +using System.Buffers; using System.Collections.Generic; using System.Text; using System.Text.Json; @@ -99,9 +100,9 @@ void writeEntity(Utf8JsonWriter writer, T entity) static string GetJson(IEnumerable values, Action writeValue, Func? mustSkipValue = null) { - using var stream = MemoryStreamManager.GetStream(); + using var stream = (RecyclableMemoryStream)MemoryStreamManager.GetStream(); - using (var jsonWriter = new Utf8JsonWriter(stream)) + using (var jsonWriter = new Utf8JsonWriter((IBufferWriter)stream)) { jsonWriter.WriteStartArray(); @@ -122,12 +123,11 @@ static string GetJson(IEnumerable values, Action writeValu jsonWriter.WriteEndArray(); } - var streamInt32Length = (int)stream.Length; #if NETSTANDARD2_0 + var streamInt32Length = (int)stream.Length; return Encoding.UTF8.GetString(stream.GetBuffer(), 0, streamInt32Length); #else - var streamBufferSpan = stream.GetBuffer().AsSpan(0, streamInt32Length); - return Encoding.UTF8.GetString(streamBufferSpan); + return Encoding.UTF8.GetString(stream.GetSpan()); #endif } } diff --git a/src/SharedProjectProperties.xml b/src/SharedProjectProperties.xml index c961c9b..b539465 100644 --- a/src/SharedProjectProperties.xml +++ b/src/SharedProjectProperties.xml @@ -1,64 +1,64 @@  - - BlazarTech.QueryableValues - BlazarTech.QueryableValues.SqlServer - true - https://github.com/yv989c/BlazarTech.QueryableValues - Carlos Villegas - BlazarTech.QueryableValues - BlazarTech.QueryableValues.SqlServer - Allows you to efficiently compose an IEnumerable<T> in your Entity Framework Core queries when using the SQL Server Database Provider. This is accomplished by using the AsQueryableValues extension method available on the DbContext class. Everything is evaluated on the server with a single round trip, in a way that preserves the query's execution plan, even when the values behind the IEnumerable<T> are changed on subsequent executions. - MIT - https://github.com/yv989c/BlazarTech.QueryableValues - Entity EF EFCore EntityFramework EntityFrameworkCore entity-framework-core Data ORM SQLServer sql-server IQueryable IEnumerable Queryable Values MemoryJoin BulkInsertTempTableAsync WhereBulkContains Extension Extensions Memory Join Contains Performance LINQ - icon.png - README.md - $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb - enable - + + BlazarTech.QueryableValues + BlazarTech.QueryableValues.SqlServer + true + https://github.com/yv989c/BlazarTech.QueryableValues + Carlos Villegas + BlazarTech.QueryableValues + BlazarTech.QueryableValues.SqlServer + Allows you to efficiently compose an IEnumerable<T> in your Entity Framework Core queries when using the SQL Server Database Provider. This is accomplished by using the AsQueryableValues extension method available on the DbContext class. Everything is evaluated on the server with a single round trip, in a way that preserves the query's execution plan, even when the values behind the IEnumerable<T> are changed on subsequent executions. + MIT + https://github.com/yv989c/BlazarTech.QueryableValues + Entity EF EFCore EntityFramework EntityFrameworkCore entity-framework-core Data ORM SQLServer sql-server IQueryable IEnumerable Queryable Values MemoryJoin BulkInsertTempTableAsync WhereBulkContains Extension Extensions Memory Join Contains Performance LINQ + icon.png + README.md + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + enable + - - + + - - True - \ - - - True - \ - - - True - \ - - + + True + \ + + + True + \ + + + True + \ + + - - true - + + true + - - - - Builders\%(RecursiveDir)\%(FileName)%(Extension) - - - Serializers\%(RecursiveDir)\%(FileName)%(Extension) - - - SqlServer\%(RecursiveDir)\%(FileName)%(Extension) - - + + + + Builders\%(RecursiveDir)\%(FileName)%(Extension) + + + Serializers\%(RecursiveDir)\%(FileName)%(Extension) + + + SqlServer\%(RecursiveDir)\%(FileName)%(Extension) + + - - - - - - + + + + + + \ No newline at end of file From 893e55249ae7f80e7c7d92c0b17cc9a5218a6d43 Mon Sep 17 00:00:00 2001 From: yv989c Date: Sat, 18 Mar 2023 13:14:30 -0400 Subject: [PATCH 03/29] fix: JsonSerializer --- src/QueryableValues.SqlServer/Serializers/JsonSerializer.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/QueryableValues.SqlServer/Serializers/JsonSerializer.cs b/src/QueryableValues.SqlServer/Serializers/JsonSerializer.cs index cb0824f..311d30e 100644 --- a/src/QueryableValues.SqlServer/Serializers/JsonSerializer.cs +++ b/src/QueryableValues.SqlServer/Serializers/JsonSerializer.cs @@ -126,8 +126,12 @@ static string GetJson(IEnumerable values, Action writeValu #if NETSTANDARD2_0 var streamInt32Length = (int)stream.Length; return Encoding.UTF8.GetString(stream.GetBuffer(), 0, streamInt32Length); +#elif NETSTANDARD2_1_OR_GREATER + stream.Position = 0; + var streamInt32Length = (int)stream.Length; + return Encoding.UTF8.GetString(stream.GetSpan()[..streamInt32Length]); #else - return Encoding.UTF8.GetString(stream.GetSpan()); + return Encoding.UTF8.GetString(stream.GetReadOnlySequence()); #endif } } From ecefd3c71f8d0dc4f597dfbd239bcbcd5de2f054 Mon Sep 17 00:00:00 2001 From: yv989c Date: Sat, 18 Mar 2023 17:25:24 -0400 Subject: [PATCH 04/29] feat: Implements JsonQueryableFactory --- .../SqlServer/JsonQueryableFactory.cs | 604 ++++++++++++++++++ 1 file changed, 604 insertions(+) create mode 100644 src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs diff --git a/src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs b/src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs new file mode 100644 index 0000000..67fa4e3 --- /dev/null +++ b/src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs @@ -0,0 +1,604 @@ +#if EFCORE +using BlazarTech.QueryableValues.Builders; +using BlazarTech.QueryableValues.Serializers; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.ObjectPool; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlTypes; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; + +namespace BlazarTech.QueryableValues.SqlServer +{ + internal sealed class JsonQueryableFactory : IQueryableFactory + { + private const string SqlSelect = "SELECT"; + private const string SqlSelectTop = "SELECT TOP({1})"; + + private static readonly ConcurrentDictionary SqlCache = new(); + private static readonly ConcurrentDictionary SelectorExpressionCache = new(); + + private static readonly DefaultObjectPool StringBuilderPool = new DefaultObjectPool( + new StringBuilderPooledObjectPolicy + { + InitialCapacity = 1024, + MaximumRetainedCapacity = 16384 + }); + + private readonly ISerializer _serializer; + private readonly QueryableValuesSqlServerOptions _options; + + public JsonQueryableFactory(IJsonSerializer serializer, QueryableValuesSqlServerOptions options) + { + if (serializer is null) + { + throw new ArgumentNullException(nameof(serializer)); + } + + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } + + _serializer = serializer; + _options = options; + } + + /// + /// Used to optimize the generated SQL by providing a TOP(n) on the SELECT statement. + /// In my tests, I observed improved memory grant estimation by SQL Server's query engine. + /// + private bool UseSelectTopOptimization(DeferredValues deferredValues) + where T : notnull + { +#if EFCORE3 + // In my EF Core 3 tests, it seems that on the first execution of the query, + // it is caching the values from the parameters provided to the FromSqlRaw method. + // This imposes a problem when trying to optimize the SQL using the HasCount property in this class. + // It is critical to know the exact number of elements behind "values" at execution time, + // this is because the number of items behind "values" can change between executions of the query, + // therefore, this optimization cannot be done in a reliable way under EF Core 3. + // + // Under EF Core 5 and 6 this is not an issue. The parameters are always evaluated on each execution. + return false; +#else + return + _options.WithUseSelectTopOptimization && + deferredValues.HasCount; +#endif + } + + private SqlParameter[] GetSqlParameters(DeferredValues deferredValues) + where T : notnull + { + SqlParameter[] sqlParameters; + + // Missing parameter names are auto-generated (p0, p1, etc.) by FromSqlRaw based on its position in the array. + var jsonParameter = new SqlParameter(null, SqlDbType.NVarChar, -1) + { + // DeferredValues allows us to defer the enumeration of values until the query is materialized. + Value = _options.WithUseDeferredEnumeration ? deferredValues : deferredValues.ToString(null) + }; + + if (UseSelectTopOptimization(deferredValues)) + { + // bigint to avoid implicit casting by the TOP operation (observed in the execution plan). + var countParameter = new SqlParameter(null, SqlDbType.BigInt) + { + Value = _options.WithUseDeferredEnumeration ? deferredValues : deferredValues.ToInt64(null) + }; + + sqlParameters = new[] { jsonParameter, countParameter }; + } + else + { + sqlParameters = new[] { jsonParameter }; + } + + return sqlParameters; + } + + private string GetSqlForSimpleTypes(string sqlType, DeferredValues deferredValues, (int Precision, int Scale)? precisionScale = null) + where T : notnull + { + var useSelectTopOptimization = UseSelectTopOptimization(deferredValues); + var cacheKey = new + { + SqlType = sqlType, + UseSelectTopOptimization = useSelectTopOptimization, + PrecisionScale = precisionScale + }; + + if (SqlCache.TryGetValue(cacheKey, out string? sql)) + { + return sql; + } + + var sqlPrefix = useSelectTopOptimization ? SqlSelectTop : SqlSelect; + var sqlTypeArguments = precisionScale.HasValue ? $"({precisionScale.Value.Precision},{precisionScale.Value.Scale})" : null; + + sql = + $"{sqlPrefix} V " + + $"FROM OPENJSON({{0}}) WITH ([V] {sqlType}{sqlTypeArguments} '$')"; + + SqlCache.TryAdd(cacheKey, sql); + + return sql; + } + + private IQueryable Create(DbContext dbContext, string sql, DeferredValues deferredValues) + where TValue : notnull + { + var sqlParameters = GetSqlParameters(deferredValues); + + var queryableValues = dbContext + .Set>() + .FromSqlRaw(sql, sqlParameters); + + return queryableValues.Select(i => i.V); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredByteValues(_serializer, values); + var sql = GetSqlForSimpleTypes("tinyint", deferredValues); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredInt16Values(_serializer, values); + var sql = GetSqlForSimpleTypes("smallint", deferredValues); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredInt32Values(_serializer, values); + var sql = GetSqlForSimpleTypes("int", deferredValues); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredInt64Values(_serializer, values); + var sql = GetSqlForSimpleTypes("bigint", deferredValues); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values, int numberOfDecimals = 4) + { + var deferredValues = new DeferredDecimalValues(_serializer, values); + var precisionScale = (38, numberOfDecimals); + var sql = GetSqlForSimpleTypes("decimal", deferredValues, precisionScale: precisionScale); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredSingleValues(_serializer, values); + var sql = GetSqlForSimpleTypes("real", deferredValues); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredDoubleValues(_serializer, values); + var sql = GetSqlForSimpleTypes("float", deferredValues); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredDateTimeValues(_serializer, values); + var sql = GetSqlForSimpleTypes("datetime2", deferredValues); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredDateTimeOffsetValues(_serializer, values); + var sql = GetSqlForSimpleTypes("datetimeoffset", deferredValues); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values, bool isUnicode = false) + { + string sql; + var deferredValues = new DeferredCharValues(_serializer, values); + + if (isUnicode) + { + sql = GetSqlForSimpleTypes("nvarchar(1)", deferredValues); + } + else + { + sql = GetSqlForSimpleTypes("varchar(1)", deferredValues); + } + + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values, bool isUnicode = false) + { + string sql; + var deferredValues = new DeferredStringValues(_serializer, values); + + if (isUnicode) + { + sql = GetSqlForSimpleTypes("nvarchar(max)", deferredValues); + } + else + { + sql = GetSqlForSimpleTypes("varchar(max)", deferredValues); + } + + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredGuidValues(_serializer, values); + var sql = GetSqlForSimpleTypes("uniqueidentifier", deferredValues); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values, Action>? configure) where TSource : notnull + { + var simpleTypeQueryable = getSimpleTypeQueryable(dbContext, values); + + if (simpleTypeQueryable != null) + { + return simpleTypeQueryable; + } + + var mappings = EntityPropertyMapping.GetMappings(); + var deferredValues = new DeferredEntityValues(_serializer, values, mappings); + var useSelectTopOptimization = UseSelectTopOptimization(deferredValues); + var sql = getSql(mappings, configure, useSelectTopOptimization); + var sqlParameters = GetSqlParameters(deferredValues); + + var source = dbContext + .Set() + .FromSqlRaw(sql, sqlParameters); + + var projected = projectQueryable(source, mappings); + + return projected; + + static string getSql(IReadOnlyList mappings, Action>? configure, bool useSelectTopOptimization) + { + IEntityOptionsBuilder entityOptions; + + if (configure != null) + { + var entityOptionsHelper = new EntityOptionsBuilder(); + configure?.Invoke(entityOptionsHelper); + entityOptions = entityOptionsHelper; + } + else + { + entityOptions = new EntityOptionsBuilder(); + } + + var cacheKey = new + { + Options = entityOptions, + UseSelectTopOptimization = useSelectTopOptimization + }; + + if (SqlCache.TryGetValue(cacheKey, out string? sqlFromCache)) + { + return sqlFromCache; + } + + var sb = StringBuilderPool.Get(); + + try + { + if (useSelectTopOptimization) + { + sb.Append(SqlSelectTop); + } + else + { + sb.Append(SqlSelect); + } + + sb.Append(' '); + + for (var i = 0; i < mappings.Count; i++) + { + if (i > 0) + { + sb.Append(", "); + } + + sb.Append('[').Append(mappings[i].Target.Name).Append(']'); + } + + sb.AppendLine(); + sb.Append("FROM OPENJSON({0}) WITH ("); + sb.AppendLine(); + + for (var i = 0; i < mappings.Count; i++) + { + var mapping = mappings[i]; + var propertyOptions = entityOptions.GetPropertyOptions(mapping.Source); + + if (i > 0) + { + sb.Append(',').AppendLine(); + } + + var targetName = mapping.Target.Name; + + sb.Append("\t[").Append(targetName).Append("] "); + + switch (mapping.TypeName) + { + case EntityPropertyTypeName.Boolean: + sb.Append("bit"); + break; + case EntityPropertyTypeName.Byte: + sb.Append("tinyint"); + break; + case EntityPropertyTypeName.Int16: + sb.Append("smallint"); + break; + case EntityPropertyTypeName.Int32: + sb.Append("int"); + break; + case EntityPropertyTypeName.Int64: + sb.Append("bigint"); + break; + case EntityPropertyTypeName.Decimal: + { + var numberOfDecimals = propertyOptions?.NumberOfDecimals ?? entityOptions.DefaultForNumberOfDecimals; + sb.Append("decimal(38, ").Append(numberOfDecimals).Append(')'); + } + break; + case EntityPropertyTypeName.Single: + sb.Append("real"); + break; + case EntityPropertyTypeName.Double: + sb.Append("float"); + break; + case EntityPropertyTypeName.DateTime: + sb.Append("datetime2"); + break; + case EntityPropertyTypeName.DateTimeOffset: + sb.Append("datetimeoffset"); + break; + case EntityPropertyTypeName.Guid: + sb.Append("uniqueidentifier"); + break; + case EntityPropertyTypeName.Char: + if ((propertyOptions?.IsUnicode ?? entityOptions.DefaultForIsUnicode) == true) + { + sb.Append("nvarchar(1)"); + } + else + { + sb.Append("varchar(1)"); + } + break; + case EntityPropertyTypeName.String: + if ((propertyOptions?.IsUnicode ?? entityOptions.DefaultForIsUnicode) == true) + { + sb.Append("nvarchar(max)"); + } + else + { + sb.Append("varchar(max)"); + } + break; + default: + throw new NotImplementedException(mapping.TypeName.ToString()); + } + } + + sb.AppendLine(); + sb.Append(')'); + + var sql = sb.ToString(); + + SqlCache.TryAdd(cacheKey, sql); + + return sql; + } + finally + { + StringBuilderPool.Return(sb); + } + } + + static IQueryable projectQueryable(IQueryable source, IReadOnlyList mappings) + { + Type sourceType = typeof(TSource); + + var queryable = getFromCache(sourceType, source); + if (queryable != null) + { + return queryable; + } + + Expression body; + var parameterExpression = Expression.Parameter(typeof(QueryableValuesEntity), "i"); + + var useConstructor = !mappings.All(i => i.Source.CanWrite); + + // Mainly for anonymous types. + if (useConstructor) + { + var constructor = sourceType.GetConstructors().FirstOrDefault(); + + if (constructor == null) + { + throw new InvalidOperationException($"Cannot find a suitable constructor in {sourceType.FullName}."); + } + + var arguments = new Expression[mappings.Count]; + var members = new MemberInfo[mappings.Count]; + + for (int i = 0; i < mappings.Count; i++) + { + var mapping = mappings[i]; + + arguments[i] = getTargetPropertyExpression(parameterExpression, mapping); + + var methodInfo = mapping.Source.GetGetMethod(true); + + if (methodInfo == null) + { + throw new InvalidOperationException($"Property {mapping.Source.Name} must have a Get accessor."); + } + + members[i] = methodInfo; + } + + body = Expression.New(constructor, arguments, members); + } + else + { + var bindings = new MemberBinding[mappings.Count]; + + for (int i = 0; i < mappings.Count; i++) + { + var mapping = mappings[i]; + + var methodInfo = mapping.Source.GetSetMethod(); + + if (methodInfo == null) + { + throw new InvalidOperationException($"Property {mapping.Source.Name} must have a Set accessor."); + } + + bindings[i] = Expression.Bind( + methodInfo, + getTargetPropertyExpression(parameterExpression, mapping) + ); + } + + var newExpression = Expression.New(sourceType); + body = Expression.MemberInit(newExpression, bindings); + } + + var bodyParameteters = new[] + { + parameterExpression + }; + + var selector = Expression.Lambda>(body, bodyParameteters); + + SelectorExpressionCache.TryAdd(sourceType, selector); + + queryable = Queryable.Select(source, selector); + + return queryable; + + #region Helpers + + static Expression getTargetPropertyExpression(ParameterExpression parameterExpression, EntityPropertyMapping mapping) + { + var propertyExpression = Expression.Property(parameterExpression, mapping.Target.Name); + + if (mapping.Source.PropertyType == mapping.Target.PropertyType) + { + return propertyExpression; + } + else + { + return Expression.Convert(propertyExpression, mapping.Source.PropertyType); + } + } + + static IQueryable? getFromCache(Type sourceType, IQueryable source) + { + if (SelectorExpressionCache.TryGetValue(sourceType, out object? selectorFromCache)) + { + var selector = (Expression>)selectorFromCache; + var queryable = Queryable.Select(source, selector); + return queryable; + } + else + { + return null; + } + } + + #endregion + } + + IQueryable? getSimpleTypeQueryable(DbContext dbContext, IEnumerable values) + { + if (EntityPropertyMapping.IsSimpleType(typeof(TSource))) + { + if (values is IEnumerable byteValues) + { + return (IQueryable)Create(dbContext, byteValues); + } + else if (values is IEnumerable int16Values) + { + return (IQueryable)Create(dbContext, int16Values); + } + else if (values is IEnumerable int32Values) + { + return (IQueryable)Create(dbContext, int32Values); + } + else if (values is IEnumerable int64Values) + { + return (IQueryable)Create(dbContext, int64Values); + } + else if (values is IEnumerable decimalValues) + { + return (IQueryable)Create(dbContext, decimalValues); + } + else if (values is IEnumerable singleValues) + { + return (IQueryable)Create(dbContext, singleValues); + } + else if (values is IEnumerable doubleValues) + { + return (IQueryable)Create(dbContext, doubleValues); + } + else if (values is IEnumerable dateTimeValues) + { + return (IQueryable)Create(dbContext, dateTimeValues); + } + else if (values is IEnumerable dateTimeOffsetValues) + { + return (IQueryable)Create(dbContext, dateTimeOffsetValues); + } + else if (values is IEnumerable guidValues) + { + return (IQueryable)Create(dbContext, guidValues); + } + else if (values is IEnumerable charValues) + { + return (IQueryable)Create(dbContext, charValues); + } + else if (values is IEnumerable stringValues) + { + return (IQueryable)Create(dbContext, stringValues); + } + else + { + throw new NotImplementedException(typeof(TSource).FullName); + } + } + else + { + return null; + } + } + } + } +} +#endif \ No newline at end of file From 0e86a9a04803af239ca6ebf0d77076283b5419a5 Mon Sep 17 00:00:00 2001 From: yv989c Date: Sun, 19 Mar 2023 18:45:04 -0400 Subject: [PATCH 05/29] feat: QueryableFactory abstraction --- .../SqlServer/QueryableFactory.cs | 464 ++++++++++++++++++ 1 file changed, 464 insertions(+) create mode 100644 src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs diff --git a/src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs b/src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs new file mode 100644 index 0000000..df75f46 --- /dev/null +++ b/src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs @@ -0,0 +1,464 @@ +#if EFCORE +using BlazarTech.QueryableValues.Builders; +using BlazarTech.QueryableValues.Serializers; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.ObjectPool; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; + +namespace BlazarTech.QueryableValues.SqlServer +{ + internal abstract class QueryableFactory : IQueryableFactory + { + protected const string SqlSelect = "SELECT"; + protected const string SqlSelectTop = "SELECT TOP({1})"; + + protected static readonly ConcurrentDictionary SqlCache = new(); + private static readonly ConcurrentDictionary SelectorExpressionCache = new(); + + protected static readonly DefaultObjectPool StringBuilderPool = new DefaultObjectPool( + new StringBuilderPooledObjectPolicy + { + InitialCapacity = 1024, + MaximumRetainedCapacity = 16384 + }); + + private readonly ISerializer _serializer; + private readonly QueryableValuesSqlServerOptions _options; + + public QueryableFactory(ISerializer serializer, QueryableValuesSqlServerOptions options) + { + if (serializer is null) + { + throw new ArgumentNullException(nameof(serializer)); + } + + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } + + _serializer = serializer; + _options = options; + } + + /// + /// Used to optimize the generated SQL by providing a TOP(n) on the SELECT statement. + /// In my tests, I observed improved memory grant estimation by SQL Server's query engine. + /// + protected bool UseSelectTopOptimization(DeferredValues deferredValues) + where T : notnull + { +#if EFCORE3 + // In my EF Core 3 tests, it seems that on the first execution of the query, + // it is caching the values from the parameters provided to the FromSqlRaw method. + // This imposes a problem when trying to optimize the SQL using the HasCount property in this class. + // It is critical to know the exact number of elements behind "values" at execution time, + // this is because the number of items behind "values" can change between executions of the query, + // therefore, this optimization cannot be done in a reliable way under EF Core 3. + // + // Under EF Core 5 and 6 this is not an issue. The parameters are always evaluated on each execution. + return false; +#else + return + _options.WithUseSelectTopOptimization && + deferredValues.HasCount; +#endif + } + + protected abstract SqlParameter GetValuesParameter(); + + private SqlParameter[] GetSqlParameters(DeferredValues deferredValues) + where T : notnull + { + SqlParameter[] sqlParameters; + + var valuesParameter = GetValuesParameter(); + + // Missing parameter names are auto-generated (p0, p1, etc.) by FromSqlRaw based on its position in the array. + valuesParameter.ParameterName = null; + + // DeferredValues allows us to defer the enumeration of values until the query is materialized. + valuesParameter.Value = _options.WithUseDeferredEnumeration ? deferredValues : deferredValues.ToString(null); + + if (UseSelectTopOptimization(deferredValues)) + { + // bigint to avoid implicit casting by the TOP operation (observed in the execution plan). + var countParameter = new SqlParameter(null, SqlDbType.BigInt) + { + Value = _options.WithUseDeferredEnumeration ? deferredValues : deferredValues.ToInt64(null) + }; + + sqlParameters = new[] { valuesParameter, countParameter }; + } + else + { + sqlParameters = new[] { valuesParameter }; + } + + return sqlParameters; + } + + protected abstract string GetSqlForSimpleTypesByte(DeferredValues deferredValues); + protected abstract string GetSqlForSimpleTypesInt16(DeferredValues deferredValues); + protected abstract string GetSqlForSimpleTypesInt32(DeferredValues deferredValues); + protected abstract string GetSqlForSimpleTypesInt64(DeferredValues deferredValues); + protected abstract string GetSqlForSimpleTypesDecimal(DeferredValues deferredValues, (int Precision, int Scale) precisionScale); + protected abstract string GetSqlForSimpleTypesSingle(DeferredValues deferredValues); + protected abstract string GetSqlForSimpleTypesDouble(DeferredValues deferredValues); + protected abstract string GetSqlForSimpleTypesDateTime(DeferredValues deferredValues); + protected abstract string GetSqlForSimpleTypesDateTimeOffset(DeferredValues deferredValues); + protected abstract string GetSqlForSimpleTypesChar(DeferredValues deferredValues, bool isUnicode); + protected abstract string GetSqlForSimpleTypesString(DeferredValues deferredValues, bool isUnicode); + protected abstract string GetSqlForSimpleTypesGuid(DeferredValues deferredValues); + + private IQueryable Create(DbContext dbContext, string sql, DeferredValues deferredValues) + where TValue : notnull + { + var sqlParameters = GetSqlParameters(deferredValues); + + var queryableValues = dbContext + .Set>() + .FromSqlRaw(sql, sqlParameters); + + return queryableValues.Select(i => i.V); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredByteValues(_serializer, values); + var sql = GetSqlForSimpleTypesByte(deferredValues); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredInt16Values(_serializer, values); + var sql = GetSqlForSimpleTypesInt16(deferredValues); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredInt32Values(_serializer, values); + var sql = GetSqlForSimpleTypesInt32(deferredValues); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredInt64Values(_serializer, values); + var sql = GetSqlForSimpleTypesInt64(deferredValues); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values, int numberOfDecimals = 4) + { + var deferredValues = new DeferredDecimalValues(_serializer, values); + var precisionScale = (38, numberOfDecimals); + var sql = GetSqlForSimpleTypesDecimal(deferredValues, precisionScale); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredSingleValues(_serializer, values); + var sql = GetSqlForSimpleTypesSingle(deferredValues); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredDoubleValues(_serializer, values); + var sql = GetSqlForSimpleTypesDouble(deferredValues); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredDateTimeValues(_serializer, values); + var sql = GetSqlForSimpleTypesDateTime(deferredValues); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredDateTimeOffsetValues(_serializer, values); + var sql = GetSqlForSimpleTypesDateTimeOffset(deferredValues); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values, bool isUnicode = false) + { + var deferredValues = new DeferredCharValues(_serializer, values); + var sql = GetSqlForSimpleTypesChar(deferredValues, isUnicode); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values, bool isUnicode = false) + { + var deferredValues = new DeferredStringValues(_serializer, values); + var sql = GetSqlForSimpleTypesString(deferredValues, isUnicode); + return Create(dbContext, sql, deferredValues); + } + + public IQueryable Create(DbContext dbContext, IEnumerable values) + { + var deferredValues = new DeferredGuidValues(_serializer, values); + var sql = GetSqlForSimpleTypesGuid(deferredValues); + return Create(dbContext, sql, deferredValues); + } + + protected abstract string GetSqlForComplexTypes( + IEntityOptionsBuilder entityOptions, + bool useSelectTopOptimization, + IReadOnlyList mappings + ); + + public IQueryable Create(DbContext dbContext, IEnumerable values, Action>? configure) + where TSource : notnull + { + var simpleTypeQueryable = getSimpleTypeQueryable(dbContext, values); + + if (simpleTypeQueryable != null) + { + return simpleTypeQueryable; + } + + var mappings = EntityPropertyMapping.GetMappings(); + var deferredValues = new DeferredEntityValues(_serializer, values, mappings); + var useSelectTopOptimization = UseSelectTopOptimization(deferredValues); + var sql = getSql(mappings, configure, useSelectTopOptimization); + var sqlParameters = GetSqlParameters(deferredValues); + + var source = dbContext + .Set() + .FromSqlRaw(sql, sqlParameters); + + var projected = projectQueryable(source, mappings); + + return projected; + + string getSql(IReadOnlyList mappings, Action>? configure, bool useSelectTopOptimization) + { + IEntityOptionsBuilder entityOptions; + + if (configure != null) + { + var entityOptionsHelper = new EntityOptionsBuilder(); + configure?.Invoke(entityOptionsHelper); + entityOptions = entityOptionsHelper; + } + else + { + entityOptions = new EntityOptionsBuilder(); + } + + var cacheKey = new + { + Options = entityOptions, + UseSelectTopOptimization = useSelectTopOptimization + }; + + if (SqlCache.TryGetValue(cacheKey, out string? sqlFromCache)) + { + return sqlFromCache; + } + + var sql = GetSqlForComplexTypes(entityOptions, useSelectTopOptimization, mappings); + + SqlCache.TryAdd(cacheKey, sql); + + return sql; + } + + static IQueryable projectQueryable(IQueryable source, IReadOnlyList mappings) + { + Type sourceType = typeof(TSource); + + var queryable = getFromCache(sourceType, source); + if (queryable != null) + { + return queryable; + } + + Expression body; + var parameterExpression = Expression.Parameter(typeof(QueryableValuesEntity), "i"); + + var useConstructor = !mappings.All(i => i.Source.CanWrite); + + // Mainly for anonymous types. + if (useConstructor) + { + var constructor = sourceType.GetConstructors().FirstOrDefault(); + + if (constructor == null) + { + throw new InvalidOperationException($"Cannot find a suitable constructor in {sourceType.FullName}."); + } + + var arguments = new Expression[mappings.Count]; + var members = new MemberInfo[mappings.Count]; + + for (int i = 0; i < mappings.Count; i++) + { + var mapping = mappings[i]; + + arguments[i] = getTargetPropertyExpression(parameterExpression, mapping); + + var methodInfo = mapping.Source.GetGetMethod(true); + + if (methodInfo == null) + { + throw new InvalidOperationException($"Property {mapping.Source.Name} must have a Get accessor."); + } + + members[i] = methodInfo; + } + + body = Expression.New(constructor, arguments, members); + } + else + { + var bindings = new MemberBinding[mappings.Count]; + + for (int i = 0; i < mappings.Count; i++) + { + var mapping = mappings[i]; + + var methodInfo = mapping.Source.GetSetMethod(); + + if (methodInfo == null) + { + throw new InvalidOperationException($"Property {mapping.Source.Name} must have a Set accessor."); + } + + bindings[i] = Expression.Bind( + methodInfo, + getTargetPropertyExpression(parameterExpression, mapping) + ); + } + + var newExpression = Expression.New(sourceType); + body = Expression.MemberInit(newExpression, bindings); + } + + var bodyParameteters = new[] + { + parameterExpression + }; + + var selector = Expression.Lambda>(body, bodyParameteters); + + SelectorExpressionCache.TryAdd(sourceType, selector); + + queryable = Queryable.Select(source, selector); + + return queryable; + + #region Helpers + + static Expression getTargetPropertyExpression(ParameterExpression parameterExpression, EntityPropertyMapping mapping) + { + var propertyExpression = Expression.Property(parameterExpression, mapping.Target.Name); + + if (mapping.Source.PropertyType == mapping.Target.PropertyType) + { + return propertyExpression; + } + else + { + return Expression.Convert(propertyExpression, mapping.Source.PropertyType); + } + } + + static IQueryable? getFromCache(Type sourceType, IQueryable source) + { + if (SelectorExpressionCache.TryGetValue(sourceType, out object? selectorFromCache)) + { + var selector = (Expression>)selectorFromCache; + var queryable = Queryable.Select(source, selector); + return queryable; + } + else + { + return null; + } + } + + #endregion + } + + IQueryable? getSimpleTypeQueryable(DbContext dbContext, IEnumerable values) + { + if (EntityPropertyMapping.IsSimpleType(typeof(TSource))) + { + if (values is IEnumerable byteValues) + { + return (IQueryable)Create(dbContext, byteValues); + } + else if (values is IEnumerable int16Values) + { + return (IQueryable)Create(dbContext, int16Values); + } + else if (values is IEnumerable int32Values) + { + return (IQueryable)Create(dbContext, int32Values); + } + else if (values is IEnumerable int64Values) + { + return (IQueryable)Create(dbContext, int64Values); + } + else if (values is IEnumerable decimalValues) + { + return (IQueryable)Create(dbContext, decimalValues); + } + else if (values is IEnumerable singleValues) + { + return (IQueryable)Create(dbContext, singleValues); + } + else if (values is IEnumerable doubleValues) + { + return (IQueryable)Create(dbContext, doubleValues); + } + else if (values is IEnumerable dateTimeValues) + { + return (IQueryable)Create(dbContext, dateTimeValues); + } + else if (values is IEnumerable dateTimeOffsetValues) + { + return (IQueryable)Create(dbContext, dateTimeOffsetValues); + } + else if (values is IEnumerable guidValues) + { + return (IQueryable)Create(dbContext, guidValues); + } + else if (values is IEnumerable charValues) + { + return (IQueryable)Create(dbContext, charValues); + } + else if (values is IEnumerable stringValues) + { + return (IQueryable)Create(dbContext, stringValues); + } + else + { + throw new NotImplementedException(typeof(TSource).FullName); + } + } + else + { + return null; + } + } + } + } +} +#endif \ No newline at end of file From f8ebde94665c95a3c661d0875c4d5ef0d89925aa Mon Sep 17 00:00:00 2001 From: yv989c Date: Sun, 19 Mar 2023 18:46:22 -0400 Subject: [PATCH 06/29] refactor: Json/Xml queryable factory --- .../SqlServer/JsonQueryableFactory.cs | 613 ++++-------------- .../SqlServer/XmlQueryableFactory.cs | 606 ++++------------- 2 files changed, 237 insertions(+), 982 deletions(-) diff --git a/src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs b/src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs index 67fa4e3..e03121c 100644 --- a/src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs +++ b/src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs @@ -2,106 +2,22 @@ using BlazarTech.QueryableValues.Builders; using BlazarTech.QueryableValues.Serializers; using Microsoft.Data.SqlClient; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.ObjectPool; using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Data; -using System.Data.SqlTypes; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Text; namespace BlazarTech.QueryableValues.SqlServer { - internal sealed class JsonQueryableFactory : IQueryableFactory + internal sealed class JsonQueryableFactory : QueryableFactory { - private const string SqlSelect = "SELECT"; - private const string SqlSelectTop = "SELECT TOP({1})"; - - private static readonly ConcurrentDictionary SqlCache = new(); - private static readonly ConcurrentDictionary SelectorExpressionCache = new(); - - private static readonly DefaultObjectPool StringBuilderPool = new DefaultObjectPool( - new StringBuilderPooledObjectPolicy - { - InitialCapacity = 1024, - MaximumRetainedCapacity = 16384 - }); - - private readonly ISerializer _serializer; - private readonly QueryableValuesSqlServerOptions _options; - public JsonQueryableFactory(IJsonSerializer serializer, QueryableValuesSqlServerOptions options) + : base(serializer, options) { - if (serializer is null) - { - throw new ArgumentNullException(nameof(serializer)); - } - - if (options is null) - { - throw new ArgumentNullException(nameof(options)); - } - - _serializer = serializer; - _options = options; } - /// - /// Used to optimize the generated SQL by providing a TOP(n) on the SELECT statement. - /// In my tests, I observed improved memory grant estimation by SQL Server's query engine. - /// - private bool UseSelectTopOptimization(DeferredValues deferredValues) - where T : notnull + protected override SqlParameter GetValuesParameter() { -#if EFCORE3 - // In my EF Core 3 tests, it seems that on the first execution of the query, - // it is caching the values from the parameters provided to the FromSqlRaw method. - // This imposes a problem when trying to optimize the SQL using the HasCount property in this class. - // It is critical to know the exact number of elements behind "values" at execution time, - // this is because the number of items behind "values" can change between executions of the query, - // therefore, this optimization cannot be done in a reliable way under EF Core 3. - // - // Under EF Core 5 and 6 this is not an issue. The parameters are always evaluated on each execution. - return false; -#else - return - _options.WithUseSelectTopOptimization && - deferredValues.HasCount; -#endif - } - - private SqlParameter[] GetSqlParameters(DeferredValues deferredValues) - where T : notnull - { - SqlParameter[] sqlParameters; - - // Missing parameter names are auto-generated (p0, p1, etc.) by FromSqlRaw based on its position in the array. - var jsonParameter = new SqlParameter(null, SqlDbType.NVarChar, -1) - { - // DeferredValues allows us to defer the enumeration of values until the query is materialized. - Value = _options.WithUseDeferredEnumeration ? deferredValues : deferredValues.ToString(null) - }; - - if (UseSelectTopOptimization(deferredValues)) - { - // bigint to avoid implicit casting by the TOP operation (observed in the execution plan). - var countParameter = new SqlParameter(null, SqlDbType.BigInt) - { - Value = _options.WithUseDeferredEnumeration ? deferredValues : deferredValues.ToInt64(null) - }; - - sqlParameters = new[] { jsonParameter, countParameter }; - } - else - { - sqlParameters = new[] { jsonParameter }; - } - - return sqlParameters; + return new SqlParameter(null, SqlDbType.NVarChar, -1); } private string GetSqlForSimpleTypes(string sqlType, DeferredValues deferredValues, (int Precision, int Scale)? precisionScale = null) @@ -132,471 +48,182 @@ private string GetSqlForSimpleTypes(string sqlType, DeferredValues deferre return sql; } - private IQueryable Create(DbContext dbContext, string sql, DeferredValues deferredValues) - where TValue : notnull + protected override string GetSqlForSimpleTypesByte(DeferredValues deferredValues) { - var sqlParameters = GetSqlParameters(deferredValues); - - var queryableValues = dbContext - .Set>() - .FromSqlRaw(sql, sqlParameters); - - return queryableValues.Select(i => i.V); + return GetSqlForSimpleTypes("tinyint", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesInt16(DeferredValues deferredValues) { - var deferredValues = new DeferredByteValues(_serializer, values); - var sql = GetSqlForSimpleTypes("tinyint", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("smallint", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesInt32(DeferredValues deferredValues) { - var deferredValues = new DeferredInt16Values(_serializer, values); - var sql = GetSqlForSimpleTypes("smallint", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("int", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesInt64(DeferredValues deferredValues) { - var deferredValues = new DeferredInt32Values(_serializer, values); - var sql = GetSqlForSimpleTypes("int", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("bigint", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesDecimal(DeferredValues deferredValues, (int Precision, int Scale) precisionScale) { - var deferredValues = new DeferredInt64Values(_serializer, values); - var sql = GetSqlForSimpleTypes("bigint", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("decimal", deferredValues, precisionScale: precisionScale); } - public IQueryable Create(DbContext dbContext, IEnumerable values, int numberOfDecimals = 4) + protected override string GetSqlForSimpleTypesSingle(DeferredValues deferredValues) { - var deferredValues = new DeferredDecimalValues(_serializer, values); - var precisionScale = (38, numberOfDecimals); - var sql = GetSqlForSimpleTypes("decimal", deferredValues, precisionScale: precisionScale); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("real", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesDouble(DeferredValues deferredValues) { - var deferredValues = new DeferredSingleValues(_serializer, values); - var sql = GetSqlForSimpleTypes("real", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("float", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesDateTime(DeferredValues deferredValues) { - var deferredValues = new DeferredDoubleValues(_serializer, values); - var sql = GetSqlForSimpleTypes("float", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("datetime2", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesDateTimeOffset(DeferredValues deferredValues) { - var deferredValues = new DeferredDateTimeValues(_serializer, values); - var sql = GetSqlForSimpleTypes("datetime2", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("datetimeoffset", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesChar(DeferredValues deferredValues, bool isUnicode) { - var deferredValues = new DeferredDateTimeOffsetValues(_serializer, values); - var sql = GetSqlForSimpleTypes("datetimeoffset", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes(isUnicode ? "nvarchar(1)" : "varchar(1)", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values, bool isUnicode = false) + protected override string GetSqlForSimpleTypesString(DeferredValues deferredValues, bool isUnicode) { - string sql; - var deferredValues = new DeferredCharValues(_serializer, values); - - if (isUnicode) - { - sql = GetSqlForSimpleTypes("nvarchar(1)", deferredValues); - } - else - { - sql = GetSqlForSimpleTypes("varchar(1)", deferredValues); - } - - return Create(dbContext, sql, deferredValues); - } - - public IQueryable Create(DbContext dbContext, IEnumerable values, bool isUnicode = false) - { - string sql; - var deferredValues = new DeferredStringValues(_serializer, values); - - if (isUnicode) - { - sql = GetSqlForSimpleTypes("nvarchar(max)", deferredValues); - } - else - { - sql = GetSqlForSimpleTypes("varchar(max)", deferredValues); - } - - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes(isUnicode ? "nvarchar(max)" : "varchar(max)", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesGuid(DeferredValues deferredValues) { - var deferredValues = new DeferredGuidValues(_serializer, values); - var sql = GetSqlForSimpleTypes("uniqueidentifier", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("uniqueidentifier", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values, Action>? configure) where TSource : notnull + protected override string GetSqlForComplexTypes(IEntityOptionsBuilder entityOptions, bool useSelectTopOptimization, IReadOnlyList mappings) { - var simpleTypeQueryable = getSimpleTypeQueryable(dbContext, values); - - if (simpleTypeQueryable != null) - { - return simpleTypeQueryable; - } - - var mappings = EntityPropertyMapping.GetMappings(); - var deferredValues = new DeferredEntityValues(_serializer, values, mappings); - var useSelectTopOptimization = UseSelectTopOptimization(deferredValues); - var sql = getSql(mappings, configure, useSelectTopOptimization); - var sqlParameters = GetSqlParameters(deferredValues); + var sb = StringBuilderPool.Get(); - var source = dbContext - .Set() - .FromSqlRaw(sql, sqlParameters); - - var projected = projectQueryable(source, mappings); - - return projected; - - static string getSql(IReadOnlyList mappings, Action>? configure, bool useSelectTopOptimization) + try { - IEntityOptionsBuilder entityOptions; - - if (configure != null) + if (useSelectTopOptimization) { - var entityOptionsHelper = new EntityOptionsBuilder(); - configure?.Invoke(entityOptionsHelper); - entityOptions = entityOptionsHelper; + sb.Append(SqlSelectTop); } else { - entityOptions = new EntityOptionsBuilder(); - } - - var cacheKey = new - { - Options = entityOptions, - UseSelectTopOptimization = useSelectTopOptimization - }; - - if (SqlCache.TryGetValue(cacheKey, out string? sqlFromCache)) - { - return sqlFromCache; + sb.Append(SqlSelect); } - var sb = StringBuilderPool.Get(); + sb.Append(' '); - try + for (var i = 0; i < mappings.Count; i++) { - if (useSelectTopOptimization) + if (i > 0) { - sb.Append(SqlSelectTop); - } - else - { - sb.Append(SqlSelect); - } - - sb.Append(' '); - - for (var i = 0; i < mappings.Count; i++) - { - if (i > 0) - { - sb.Append(", "); - } - - sb.Append('[').Append(mappings[i].Target.Name).Append(']'); - } - - sb.AppendLine(); - sb.Append("FROM OPENJSON({0}) WITH ("); - sb.AppendLine(); - - for (var i = 0; i < mappings.Count; i++) - { - var mapping = mappings[i]; - var propertyOptions = entityOptions.GetPropertyOptions(mapping.Source); - - if (i > 0) - { - sb.Append(',').AppendLine(); - } - - var targetName = mapping.Target.Name; - - sb.Append("\t[").Append(targetName).Append("] "); - - switch (mapping.TypeName) - { - case EntityPropertyTypeName.Boolean: - sb.Append("bit"); - break; - case EntityPropertyTypeName.Byte: - sb.Append("tinyint"); - break; - case EntityPropertyTypeName.Int16: - sb.Append("smallint"); - break; - case EntityPropertyTypeName.Int32: - sb.Append("int"); - break; - case EntityPropertyTypeName.Int64: - sb.Append("bigint"); - break; - case EntityPropertyTypeName.Decimal: - { - var numberOfDecimals = propertyOptions?.NumberOfDecimals ?? entityOptions.DefaultForNumberOfDecimals; - sb.Append("decimal(38, ").Append(numberOfDecimals).Append(')'); - } - break; - case EntityPropertyTypeName.Single: - sb.Append("real"); - break; - case EntityPropertyTypeName.Double: - sb.Append("float"); - break; - case EntityPropertyTypeName.DateTime: - sb.Append("datetime2"); - break; - case EntityPropertyTypeName.DateTimeOffset: - sb.Append("datetimeoffset"); - break; - case EntityPropertyTypeName.Guid: - sb.Append("uniqueidentifier"); - break; - case EntityPropertyTypeName.Char: - if ((propertyOptions?.IsUnicode ?? entityOptions.DefaultForIsUnicode) == true) - { - sb.Append("nvarchar(1)"); - } - else - { - sb.Append("varchar(1)"); - } - break; - case EntityPropertyTypeName.String: - if ((propertyOptions?.IsUnicode ?? entityOptions.DefaultForIsUnicode) == true) - { - sb.Append("nvarchar(max)"); - } - else - { - sb.Append("varchar(max)"); - } - break; - default: - throw new NotImplementedException(mapping.TypeName.ToString()); - } + sb.Append(", "); } - sb.AppendLine(); - sb.Append(')'); - - var sql = sb.ToString(); - - SqlCache.TryAdd(cacheKey, sql); - - return sql; - } - finally - { - StringBuilderPool.Return(sb); + sb.Append('[').Append(mappings[i].Target.Name).Append(']'); } - } - static IQueryable projectQueryable(IQueryable source, IReadOnlyList mappings) - { - Type sourceType = typeof(TSource); + sb.AppendLine(); + sb.Append("FROM OPENJSON({0}) WITH ("); + sb.AppendLine(); - var queryable = getFromCache(sourceType, source); - if (queryable != null) + for (var i = 0; i < mappings.Count; i++) { - return queryable; - } - - Expression body; - var parameterExpression = Expression.Parameter(typeof(QueryableValuesEntity), "i"); - - var useConstructor = !mappings.All(i => i.Source.CanWrite); - - // Mainly for anonymous types. - if (useConstructor) - { - var constructor = sourceType.GetConstructors().FirstOrDefault(); - - if (constructor == null) - { - throw new InvalidOperationException($"Cannot find a suitable constructor in {sourceType.FullName}."); - } - - var arguments = new Expression[mappings.Count]; - var members = new MemberInfo[mappings.Count]; - - for (int i = 0; i < mappings.Count; i++) - { - var mapping = mappings[i]; - - arguments[i] = getTargetPropertyExpression(parameterExpression, mapping); - - var methodInfo = mapping.Source.GetGetMethod(true); - - if (methodInfo == null) - { - throw new InvalidOperationException($"Property {mapping.Source.Name} must have a Get accessor."); - } - - members[i] = methodInfo; + var mapping = mappings[i]; + var propertyOptions = entityOptions.GetPropertyOptions(mapping.Source); + + if (i > 0) + { + sb.Append(',').AppendLine(); + } + + var targetName = mapping.Target.Name; + + sb.Append("\t[").Append(targetName).Append("] "); + + switch (mapping.TypeName) + { + case EntityPropertyTypeName.Boolean: + sb.Append("bit"); + break; + case EntityPropertyTypeName.Byte: + sb.Append("tinyint"); + break; + case EntityPropertyTypeName.Int16: + sb.Append("smallint"); + break; + case EntityPropertyTypeName.Int32: + sb.Append("int"); + break; + case EntityPropertyTypeName.Int64: + sb.Append("bigint"); + break; + case EntityPropertyTypeName.Decimal: + { + var numberOfDecimals = propertyOptions?.NumberOfDecimals ?? entityOptions.DefaultForNumberOfDecimals; + sb.Append("decimal(38, ").Append(numberOfDecimals).Append(')'); + } + break; + case EntityPropertyTypeName.Single: + sb.Append("real"); + break; + case EntityPropertyTypeName.Double: + sb.Append("float"); + break; + case EntityPropertyTypeName.DateTime: + sb.Append("datetime2"); + break; + case EntityPropertyTypeName.DateTimeOffset: + sb.Append("datetimeoffset"); + break; + case EntityPropertyTypeName.Guid: + sb.Append("uniqueidentifier"); + break; + case EntityPropertyTypeName.Char: + if ((propertyOptions?.IsUnicode ?? entityOptions.DefaultForIsUnicode) == true) + { + sb.Append("nvarchar(1)"); + } + else + { + sb.Append("varchar(1)"); + } + break; + case EntityPropertyTypeName.String: + if ((propertyOptions?.IsUnicode ?? entityOptions.DefaultForIsUnicode) == true) + { + sb.Append("nvarchar(max)"); + } + else + { + sb.Append("varchar(max)"); + } + break; + default: + throw new NotImplementedException(mapping.TypeName.ToString()); } - - body = Expression.New(constructor, arguments, members); } - else - { - var bindings = new MemberBinding[mappings.Count]; - - for (int i = 0; i < mappings.Count; i++) - { - var mapping = mappings[i]; - var methodInfo = mapping.Source.GetSetMethod(); + sb.AppendLine(); + sb.Append(')'); - if (methodInfo == null) - { - throw new InvalidOperationException($"Property {mapping.Source.Name} must have a Set accessor."); - } - - bindings[i] = Expression.Bind( - methodInfo, - getTargetPropertyExpression(parameterExpression, mapping) - ); - } - - var newExpression = Expression.New(sourceType); - body = Expression.MemberInit(newExpression, bindings); - } - - var bodyParameteters = new[] - { - parameterExpression - }; - - var selector = Expression.Lambda>(body, bodyParameteters); - - SelectorExpressionCache.TryAdd(sourceType, selector); - - queryable = Queryable.Select(source, selector); - - return queryable; - - #region Helpers - - static Expression getTargetPropertyExpression(ParameterExpression parameterExpression, EntityPropertyMapping mapping) - { - var propertyExpression = Expression.Property(parameterExpression, mapping.Target.Name); - - if (mapping.Source.PropertyType == mapping.Target.PropertyType) - { - return propertyExpression; - } - else - { - return Expression.Convert(propertyExpression, mapping.Source.PropertyType); - } - } - - static IQueryable? getFromCache(Type sourceType, IQueryable source) - { - if (SelectorExpressionCache.TryGetValue(sourceType, out object? selectorFromCache)) - { - var selector = (Expression>)selectorFromCache; - var queryable = Queryable.Select(source, selector); - return queryable; - } - else - { - return null; - } - } - - #endregion + return sb.ToString(); } - - IQueryable? getSimpleTypeQueryable(DbContext dbContext, IEnumerable values) + finally { - if (EntityPropertyMapping.IsSimpleType(typeof(TSource))) - { - if (values is IEnumerable byteValues) - { - return (IQueryable)Create(dbContext, byteValues); - } - else if (values is IEnumerable int16Values) - { - return (IQueryable)Create(dbContext, int16Values); - } - else if (values is IEnumerable int32Values) - { - return (IQueryable)Create(dbContext, int32Values); - } - else if (values is IEnumerable int64Values) - { - return (IQueryable)Create(dbContext, int64Values); - } - else if (values is IEnumerable decimalValues) - { - return (IQueryable)Create(dbContext, decimalValues); - } - else if (values is IEnumerable singleValues) - { - return (IQueryable)Create(dbContext, singleValues); - } - else if (values is IEnumerable doubleValues) - { - return (IQueryable)Create(dbContext, doubleValues); - } - else if (values is IEnumerable dateTimeValues) - { - return (IQueryable)Create(dbContext, dateTimeValues); - } - else if (values is IEnumerable dateTimeOffsetValues) - { - return (IQueryable)Create(dbContext, dateTimeOffsetValues); - } - else if (values is IEnumerable guidValues) - { - return (IQueryable)Create(dbContext, guidValues); - } - else if (values is IEnumerable charValues) - { - return (IQueryable)Create(dbContext, charValues); - } - else if (values is IEnumerable stringValues) - { - return (IQueryable)Create(dbContext, stringValues); - } - else - { - throw new NotImplementedException(typeof(TSource).FullName); - } - } - else - { - return null; - } + StringBuilderPool.Return(sb); } } } diff --git a/src/QueryableValues.SqlServer/SqlServer/XmlQueryableFactory.cs b/src/QueryableValues.SqlServer/SqlServer/XmlQueryableFactory.cs index 654f432..5d65c34 100644 --- a/src/QueryableValues.SqlServer/SqlServer/XmlQueryableFactory.cs +++ b/src/QueryableValues.SqlServer/SqlServer/XmlQueryableFactory.cs @@ -2,105 +2,22 @@ using BlazarTech.QueryableValues.Builders; using BlazarTech.QueryableValues.Serializers; using Microsoft.Data.SqlClient; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.ObjectPool; using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Data; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Text; namespace BlazarTech.QueryableValues.SqlServer { - internal sealed class XmlQueryableFactory : IQueryableFactory + internal sealed class XmlQueryableFactory : QueryableFactory { - private const string SqlSelect = "SELECT"; - private const string SqlSelectTop = "SELECT TOP({1})"; - - private static readonly ConcurrentDictionary SqlCache = new(); - private static readonly ConcurrentDictionary SelectorExpressionCache = new(); - - private static readonly DefaultObjectPool StringBuilderPool = new DefaultObjectPool( - new StringBuilderPooledObjectPolicy - { - InitialCapacity = 1024, - MaximumRetainedCapacity = 16384 - }); - - private readonly IXmlSerializer _xmlSerializer; - private readonly QueryableValuesSqlServerOptions _options; - - public XmlQueryableFactory(IXmlSerializer xmlSerializer, QueryableValuesSqlServerOptions options) + public XmlQueryableFactory(IXmlSerializer serializer, QueryableValuesSqlServerOptions options) + : base(serializer, options) { - if (xmlSerializer is null) - { - throw new ArgumentNullException(nameof(xmlSerializer)); - } - - if (options is null) - { - throw new ArgumentNullException(nameof(options)); - } - - _xmlSerializer = xmlSerializer; - _options = options; } - /// - /// Used to optimize the generated SQL by providing a TOP(n) on the SELECT statement. - /// In my tests, I observed improved memory grant estimation by SQL Server's query engine. - /// - private bool UseSelectTopOptimization(DeferredValues deferredValues) - where T : notnull + protected override SqlParameter GetValuesParameter() { -#if EFCORE3 - // In my EF Core 3 tests, it seems that on the first execution of the query, - // it is caching the values from the parameters provided to the FromSqlRaw method. - // This imposes a problem when trying to optimize the SQL using the HasCount property in this class. - // It is critical to know the exact number of elements behind "values" at execution time, - // this is because the number of items behind "values" can change between executions of the query, - // therefore, this optimization cannot be done in a reliable way under EF Core 3. - // - // Under EF Core 5 and 6 this is not an issue. The parameters are always evaluated on each execution. - return false; -#else - return - _options.WithUseSelectTopOptimization && - deferredValues.HasCount; -#endif - } - - private SqlParameter[] GetSqlParameters(DeferredValues deferredValues) - where T : notnull - { - SqlParameter[] sqlParameters; - - // Missing parameter names are auto-generated (p0, p1, etc.) by FromSqlRaw based on its position in the array. - var xmlParameter = new SqlParameter(null, SqlDbType.Xml) - { - // DeferredValues allows us to defer the enumeration of values until the query is materialized. - Value = _options.WithUseDeferredEnumeration ? deferredValues : deferredValues.ToString(null) - }; - - if (UseSelectTopOptimization(deferredValues)) - { - // bigint to avoid implicit casting by the TOP operation (observed in the execution plan). - var countParameter = new SqlParameter(null, SqlDbType.BigInt) - { - Value = _options.WithUseDeferredEnumeration ? deferredValues : deferredValues.ToInt64(null) - }; - - sqlParameters = new[] { xmlParameter, countParameter }; - } - else - { - sqlParameters = new[] { xmlParameter }; - } - - return sqlParameters; + return new SqlParameter(null, SqlDbType.Xml); } private string GetSqlForSimpleTypes(string xmlType, string sqlType, DeferredValues deferredValues, (int Precision, int Scale)? precisionScale = null) @@ -132,459 +49,170 @@ private string GetSqlForSimpleTypes(string xmlType, string sqlType, DeferredV return sql; } - private IQueryable Create(DbContext dbContext, string sql, DeferredValues deferredValues) - where TValue : notnull - { - var sqlParameters = GetSqlParameters(deferredValues); - - var queryableValues = dbContext - .Set>() - .FromSqlRaw(sql, sqlParameters); - - return queryableValues.Select(i => i.V); - } - - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesByte(DeferredValues deferredValues) { - var deferredValues = new DeferredByteValues(_xmlSerializer, values); - var sql = GetSqlForSimpleTypes("unsignedByte", "tinyint", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("unsignedByte", "tinyint", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesInt16(DeferredValues deferredValues) { - var deferredValues = new DeferredInt16Values(_xmlSerializer, values); - var sql = GetSqlForSimpleTypes("short", "smallint", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("short", "smallint", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesInt32(DeferredValues deferredValues) { - var deferredValues = new DeferredInt32Values(_xmlSerializer, values); - var sql = GetSqlForSimpleTypes("integer", "int", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("integer", "int", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesInt64(DeferredValues deferredValues) { - var deferredValues = new DeferredInt64Values(_xmlSerializer, values); - var sql = GetSqlForSimpleTypes("integer", "bigint", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("integer", "bigint", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values, int numberOfDecimals = 4) + protected override string GetSqlForSimpleTypesDecimal(DeferredValues deferredValues, (int Precision, int Scale) precisionScale) { - var deferredValues = new DeferredDecimalValues(_xmlSerializer, values); - var precisionScale = (38, numberOfDecimals); - var sql = GetSqlForSimpleTypes("decimal", "decimal", deferredValues, precisionScale: precisionScale); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("decimal", "decimal", deferredValues, precisionScale: precisionScale); } - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesSingle(DeferredValues deferredValues) { - var deferredValues = new DeferredSingleValues(_xmlSerializer, values); - var sql = GetSqlForSimpleTypes("float", "real", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("float", "real", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesDouble(DeferredValues deferredValues) { - var deferredValues = new DeferredDoubleValues(_xmlSerializer, values); - var sql = GetSqlForSimpleTypes("double", "float", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("double", "float", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesDateTime(DeferredValues deferredValues) { - var deferredValues = new DeferredDateTimeValues(_xmlSerializer, values); - var sql = GetSqlForSimpleTypes("dateTime", "datetime2", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("dateTime", "datetime2", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesDateTimeOffset(DeferredValues deferredValues) { - var deferredValues = new DeferredDateTimeOffsetValues(_xmlSerializer, values); - var sql = GetSqlForSimpleTypes("dateTime", "datetimeoffset", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("dateTime", "datetimeoffset", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values, bool isUnicode = false) + protected override string GetSqlForSimpleTypesChar(DeferredValues deferredValues, bool isUnicode) { - string sql; - var deferredValues = new DeferredCharValues(_xmlSerializer, values); - - if (isUnicode) - { - sql = GetSqlForSimpleTypes("string", "nvarchar(1)", deferredValues); - } - else - { - sql = GetSqlForSimpleTypes("string", "varchar(1)", deferredValues); - } - - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("string", isUnicode ? "nvarchar(1)" : "varchar(1)", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values, bool isUnicode = false) + protected override string GetSqlForSimpleTypesString(DeferredValues deferredValues, bool isUnicode) { - string sql; - var deferredValues = new DeferredStringValues(_xmlSerializer, values); - - if (isUnicode) - { - sql = GetSqlForSimpleTypes("string", "nvarchar(max)", deferredValues); - } - else - { - sql = GetSqlForSimpleTypes("string", "varchar(max)", deferredValues); - } - - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("string", isUnicode ? "nvarchar(max)" : "varchar(max)", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values) + protected override string GetSqlForSimpleTypesGuid(DeferredValues deferredValues) { - var deferredValues = new DeferredGuidValues(_xmlSerializer, values); - var sql = GetSqlForSimpleTypes("string", "uniqueidentifier", deferredValues); - return Create(dbContext, sql, deferredValues); + return GetSqlForSimpleTypes("string", "uniqueidentifier", deferredValues); } - public IQueryable Create(DbContext dbContext, IEnumerable values, Action>? configure) where TSource : notnull + protected override string GetSqlForComplexTypes(IEntityOptionsBuilder entityOptions, bool useSelectTopOptimization, IReadOnlyList mappings) { - var simpleTypeQueryable = getSimpleTypeQueryable(dbContext, values); - - if (simpleTypeQueryable != null) - { - return simpleTypeQueryable; - } - - var mappings = EntityPropertyMapping.GetMappings(); - var deferredValues = new DeferredEntityValues(_xmlSerializer, values, mappings); - var useSelectTopOptimization = UseSelectTopOptimization(deferredValues); - var sql = getSql(mappings, configure, useSelectTopOptimization); - var sqlParameters = GetSqlParameters(deferredValues); - - var source = dbContext - .Set() - .FromSqlRaw(sql, sqlParameters); - - var projected = projectQueryable(source, mappings); - - return projected; + var sb = StringBuilderPool.Get(); - static string getSql(IReadOnlyList mappings, Action>? configure, bool useSelectTopOptimization) + try { - IEntityOptionsBuilder entityOptions; - - if (configure != null) + if (useSelectTopOptimization) { - var entityOptionsHelper = new EntityOptionsBuilder(); - configure?.Invoke(entityOptionsHelper); - entityOptions = entityOptionsHelper; + sb.Append(SqlSelectTop); } else { - entityOptions = new EntityOptionsBuilder(); + sb.Append(SqlSelect); } - var cacheKey = new - { - Options = entityOptions, - UseSelectTopOptimization = useSelectTopOptimization - }; + sb.AppendLine(); - if (SqlCache.TryGetValue(cacheKey, out string? sqlFromCache)) + for (int i = 0; i < mappings.Count; i++) { - return sqlFromCache; + var mapping = mappings[i]; + var propertyOptions = entityOptions.GetPropertyOptions(mapping.Source); + + if (i > 0) + { + sb.Append(',').AppendLine(); + } + + var targetName = mapping.Target.Name; + + sb.Append("\tI.value('@").Append(targetName).Append("[1] cast as "); + + switch (mapping.TypeName) + { + case EntityPropertyTypeName.Boolean: + sb.Append("xs:boolean?', 'bit'"); + break; + case EntityPropertyTypeName.Byte: + sb.Append("xs:unsignedByte?', 'tinyint'"); + break; + case EntityPropertyTypeName.Int16: + sb.Append("xs:short?', 'smallint'"); + break; + case EntityPropertyTypeName.Int32: + sb.Append("xs:integer?', 'int'"); + break; + case EntityPropertyTypeName.Int64: + sb.Append("xs:integer?', 'bigint'"); + break; + case EntityPropertyTypeName.Decimal: + { + var numberOfDecimals = propertyOptions?.NumberOfDecimals ?? entityOptions.DefaultForNumberOfDecimals; + sb.Append("xs:decimal?', 'decimal(38, ").Append(numberOfDecimals).Append(")'"); + } + break; + case EntityPropertyTypeName.Single: + sb.Append("xs:float?', 'real'"); + break; + case EntityPropertyTypeName.Double: + sb.Append("xs:double?', 'float'"); + break; + case EntityPropertyTypeName.DateTime: + sb.Append("xs:dateTime?', 'datetime2'"); + break; + case EntityPropertyTypeName.DateTimeOffset: + sb.Append("xs:dateTime?', 'datetimeoffset'"); + break; + case EntityPropertyTypeName.Guid: + sb.Append("xs:string?', 'uniqueidentifier'"); + break; + case EntityPropertyTypeName.Char: + if ((propertyOptions?.IsUnicode ?? entityOptions.DefaultForIsUnicode) == true) + { + sb.Append("xs:string?', 'nvarchar(1)'"); + } + else + { + sb.Append("xs:string?', 'varchar(1)'"); + } + break; + case EntityPropertyTypeName.String: + if ((propertyOptions?.IsUnicode ?? entityOptions.DefaultForIsUnicode) == true) + { + sb.Append("xs:string?', 'nvarchar(max)'"); + } + else + { + sb.Append("xs:string?', 'varchar(max)'"); + } + break; + default: + throw new NotImplementedException(mapping.TypeName.ToString()); + } + + sb.Append(") AS [").Append(targetName).Append(']'); } - var sb = StringBuilderPool.Get(); - - try - { - if (useSelectTopOptimization) - { - sb.Append(SqlSelectTop); - } - else - { - sb.Append(SqlSelect); - } - - sb.AppendLine(); + sb.AppendLine(); + sb.Append("FROM {0}.nodes('/R/V') N(I)"); - for (int i = 0; i < mappings.Count; i++) - { - var mapping = mappings[i]; - var propertyOptions = entityOptions.GetPropertyOptions(mapping.Source); - - if (i > 0) - { - sb.Append(',').AppendLine(); - } - - var targetName = mapping.Target.Name; - - sb.Append("\tI.value('@").Append(targetName).Append("[1] cast as "); - - switch (mapping.TypeName) - { - case EntityPropertyTypeName.Boolean: - sb.Append("xs:boolean?', 'bit'"); - break; - case EntityPropertyTypeName.Byte: - sb.Append("xs:unsignedByte?', 'tinyint'"); - break; - case EntityPropertyTypeName.Int16: - sb.Append("xs:short?', 'smallint'"); - break; - case EntityPropertyTypeName.Int32: - sb.Append("xs:integer?', 'int'"); - break; - case EntityPropertyTypeName.Int64: - sb.Append("xs:integer?', 'bigint'"); - break; - case EntityPropertyTypeName.Decimal: - { - var numberOfDecimals = propertyOptions?.NumberOfDecimals ?? entityOptions.DefaultForNumberOfDecimals; - sb.Append("xs:decimal?', 'decimal(38, ").Append(numberOfDecimals).Append(")'"); - } - break; - case EntityPropertyTypeName.Single: - sb.Append("xs:float?', 'real'"); - break; - case EntityPropertyTypeName.Double: - sb.Append("xs:double?', 'float'"); - break; - case EntityPropertyTypeName.DateTime: - sb.Append("xs:dateTime?', 'datetime2'"); - break; - case EntityPropertyTypeName.DateTimeOffset: - sb.Append("xs:dateTime?', 'datetimeoffset'"); - break; - case EntityPropertyTypeName.Guid: - sb.Append("xs:string?', 'uniqueidentifier'"); - break; - case EntityPropertyTypeName.Char: - if ((propertyOptions?.IsUnicode ?? entityOptions.DefaultForIsUnicode) == true) - { - sb.Append("xs:string?', 'nvarchar(1)'"); - } - else - { - sb.Append("xs:string?', 'varchar(1)'"); - } - break; - case EntityPropertyTypeName.String: - if ((propertyOptions?.IsUnicode ?? entityOptions.DefaultForIsUnicode) == true) - { - sb.Append("xs:string?', 'nvarchar(max)'"); - } - else - { - sb.Append("xs:string?', 'varchar(max)'"); - } - break; - default: - throw new NotImplementedException(mapping.TypeName.ToString()); - } - - sb.Append(") AS [").Append(targetName).Append(']'); - } - - sb.AppendLine(); - sb.Append("FROM {0}.nodes('/R/V') N(I)"); - - var sql = sb.ToString(); - - SqlCache.TryAdd(cacheKey, sql); - - return sql; - } - finally - { - StringBuilderPool.Return(sb); - } + return sb.ToString(); } - - static IQueryable projectQueryable(IQueryable source, IReadOnlyList mappings) + finally { - Type sourceType = typeof(TSource); - - var queryable = getFromCache(sourceType, source); - if (queryable != null) - { - return queryable; - } - - Expression body; - var parameterExpression = Expression.Parameter(typeof(QueryableValuesEntity), "i"); - - var useConstructor = !mappings.All(i => i.Source.CanWrite); - - // Mainly for anonymous types. - if (useConstructor) - { - var constructor = sourceType.GetConstructors().FirstOrDefault(); - - if (constructor == null) - { - throw new InvalidOperationException($"Cannot find a suitable constructor in {sourceType.FullName}."); - } - - var arguments = new Expression[mappings.Count]; - var members = new MemberInfo[mappings.Count]; - - for (int i = 0; i < mappings.Count; i++) - { - var mapping = mappings[i]; - - arguments[i] = getTargetPropertyExpression(parameterExpression, mapping); - - var methodInfo = mapping.Source.GetGetMethod(true); - - if (methodInfo == null) - { - throw new InvalidOperationException($"Property {mapping.Source.Name} must have a Get accessor."); - } - - members[i] = methodInfo; - } - - body = Expression.New(constructor, arguments, members); - } - else - { - var bindings = new MemberBinding[mappings.Count]; - - for (int i = 0; i < mappings.Count; i++) - { - var mapping = mappings[i]; - - var methodInfo = mapping.Source.GetSetMethod(); - - if (methodInfo == null) - { - throw new InvalidOperationException($"Property {mapping.Source.Name} must have a Set accessor."); - } - - bindings[i] = Expression.Bind( - methodInfo, - getTargetPropertyExpression(parameterExpression, mapping) - ); - } - - var newExpression = Expression.New(sourceType); - body = Expression.MemberInit(newExpression, bindings); - } - - var bodyParameteters = new[] - { - parameterExpression - }; - - var selector = Expression.Lambda>(body, bodyParameteters); - - SelectorExpressionCache.TryAdd(sourceType, selector); - - queryable = Queryable.Select(source, selector); - - return queryable; - - #region Helpers - - static Expression getTargetPropertyExpression(ParameterExpression parameterExpression, EntityPropertyMapping mapping) - { - var propertyExpression = Expression.Property(parameterExpression, mapping.Target.Name); - - if (mapping.Source.PropertyType == mapping.Target.PropertyType) - { - return propertyExpression; - } - else - { - return Expression.Convert(propertyExpression, mapping.Source.PropertyType); - } - } - - static IQueryable? getFromCache(Type sourceType, IQueryable source) - { - if (SelectorExpressionCache.TryGetValue(sourceType, out object? selectorFromCache)) - { - var selector = (Expression>)selectorFromCache; - var queryable = Queryable.Select(source, selector); - return queryable; - } - else - { - return null; - } - } - - #endregion - } - - IQueryable? getSimpleTypeQueryable(DbContext dbContext, IEnumerable values) - { - if (EntityPropertyMapping.IsSimpleType(typeof(TSource))) - { - if (values is IEnumerable byteValues) - { - return (IQueryable)Create(dbContext, byteValues); - } - else if (values is IEnumerable int16Values) - { - return (IQueryable)Create(dbContext, int16Values); - } - else if (values is IEnumerable int32Values) - { - return (IQueryable)Create(dbContext, int32Values); - } - else if (values is IEnumerable int64Values) - { - return (IQueryable)Create(dbContext, int64Values); - } - else if (values is IEnumerable decimalValues) - { - return (IQueryable)Create(dbContext, decimalValues); - } - else if (values is IEnumerable singleValues) - { - return (IQueryable)Create(dbContext, singleValues); - } - else if (values is IEnumerable doubleValues) - { - return (IQueryable)Create(dbContext, doubleValues); - } - else if (values is IEnumerable dateTimeValues) - { - return (IQueryable)Create(dbContext, dateTimeValues); - } - else if (values is IEnumerable dateTimeOffsetValues) - { - return (IQueryable)Create(dbContext, dateTimeOffsetValues); - } - else if (values is IEnumerable guidValues) - { - return (IQueryable)Create(dbContext, guidValues); - } - else if (values is IEnumerable charValues) - { - return (IQueryable)Create(dbContext, charValues); - } - else if (values is IEnumerable stringValues) - { - return (IQueryable)Create(dbContext, stringValues); - } - else - { - throw new NotImplementedException(typeof(TSource).FullName); - } - } - else - { - return null; - } + StringBuilderPool.Return(sb); } } } From ec4133cf2b30abdabed4c59be4167269ad4d2b3e Mon Sep 17 00:00:00 2001 From: yv989c Date: Sun, 19 Mar 2023 18:53:43 -0400 Subject: [PATCH 07/29] feat: serialization options --- .../QueryableValuesSqlServerExtension.cs | 19 +++++++++-- .../QueryableValuesSqlServerOptions.cs | 20 +++++++++++ .../SerializationOptions.cs | 33 +++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 src/QueryableValues.SqlServer/SerializationOptions.cs diff --git a/src/QueryableValues.SqlServer/QueryableValuesSqlServerExtension.cs b/src/QueryableValues.SqlServer/QueryableValuesSqlServerExtension.cs index 2c0d757..a66f6f8 100644 --- a/src/QueryableValues.SqlServer/QueryableValuesSqlServerExtension.cs +++ b/src/QueryableValues.SqlServer/QueryableValuesSqlServerExtension.cs @@ -49,13 +49,28 @@ public void ApplyServices(IServiceCollection services) } services.AddSingleton(); + services.AddSingleton(); services.AddScoped(sp => { var options = sp.GetRequiredService(); var extension = options.FindExtension() ?? throw new InvalidOperationException(); - var xmlSerializer = sp.GetRequiredService(); - return new SqlServer.XmlQueryableFactory(xmlSerializer, extension.Options); + + switch (extension.Options.WithSerializationOptions) + { + case SerializationOptions.UseJson: + { + var serializer = sp.GetRequiredService(); + return new SqlServer.JsonQueryableFactory(serializer, extension.Options); + } + case SerializationOptions.UseXml: + { + var serializer = sp.GetRequiredService(); + return new SqlServer.XmlQueryableFactory(serializer, extension.Options); + } + default: + throw new NotImplementedException(); + } }); } diff --git a/src/QueryableValues.SqlServer/QueryableValuesSqlServerOptions.cs b/src/QueryableValues.SqlServer/QueryableValuesSqlServerOptions.cs index 0ee1ab7..6b3bf3c 100644 --- a/src/QueryableValues.SqlServer/QueryableValuesSqlServerOptions.cs +++ b/src/QueryableValues.SqlServer/QueryableValuesSqlServerOptions.cs @@ -10,6 +10,7 @@ public sealed class QueryableValuesSqlServerOptions { internal bool WithUseSelectTopOptimization { get; private set; } = true; internal bool WithUseDeferredEnumeration { get; private set; } = true; + internal SerializationOptions WithSerializationOptions { get; private set; } = SerializationOptions.UseXml; /// /// When possible, uses a TOP(n) clause in the underlying SELECT statement to assist SQL Server memory grant estimation. The default is . @@ -26,6 +27,25 @@ public QueryableValuesSqlServerOptions UseSelectTopOptimization(bool useSelectTo return this; } + /// + /// Configures serialization options. The default is . + /// + /// Serialization options. + /// The same instance so subsequent configurations can be chained. + public QueryableValuesSqlServerOptions Serialization(SerializationOptions options = SerializationOptions.UseXml) + { + if (Enum.IsDefined(typeof(SerializationOptions), options)) + { + WithSerializationOptions = options; + } + else + { + throw new ArgumentOutOfRangeException(nameof(options)); + } + + return this; + } + #if !EFCORE3 /// /// If , the provided to any of the AsQueryableValues methods will be enumerated when the query is materialized; otherwise, it will be immediately enumerated once. The default is . diff --git a/src/QueryableValues.SqlServer/SerializationOptions.cs b/src/QueryableValues.SqlServer/SerializationOptions.cs new file mode 100644 index 0000000..d553303 --- /dev/null +++ b/src/QueryableValues.SqlServer/SerializationOptions.cs @@ -0,0 +1,33 @@ +namespace BlazarTech.QueryableValues +{ + /// + /// Serialization options. + /// + public enum SerializationOptions + { + /// + /// Use the JSON serializer. + /// + /// + /// + /// In my tests, JSON significantly outperforms XML, particularly on big sequences. + ///
+ ///
+ /// JSON can only be used when the following is true:
+ /// - The SQL Server instance is 2016 and above.
+ /// - The database has a compatibility level of 130 or higher. + ///
+ ///
+ /// More info: + ///
+ ///
+ /// WARNING: An error will occur at runtime if JSON serialization is not supported. + ///
+ UseJson = 1, + + /// + /// Use the XML serializer. + /// + UseXml = 2 + } +} From 4556101020f9f86cafe697f2edd09523af9f5b0f Mon Sep 17 00:00:00 2001 From: yv989c Date: Tue, 21 Mar 2023 00:42:26 -0400 Subject: [PATCH 08/29] refactor: queryable factory --- .../QueryableValuesDbContextExtensions.cs | 3 +- .../QueryableValuesSqlServerExtension.cs | 31 ++++------------- .../SqlServer/JsonQueryableFactory.cs | 5 +-- .../SqlServer/QueryableFactory.cs | 11 ++++--- .../SqlServer/QueryableFactoryFactory.cs | 33 +++++++++++++++++++ .../SqlServer/XmlQueryableFactory.cs | 5 +-- 6 files changed, 54 insertions(+), 34 deletions(-) create mode 100644 src/QueryableValues.SqlServer/SqlServer/QueryableFactoryFactory.cs diff --git a/src/QueryableValues.SqlServer/QueryableValuesDbContextExtensions.cs b/src/QueryableValues.SqlServer/QueryableValuesDbContextExtensions.cs index 8147f6f..86cc049 100644 --- a/src/QueryableValues.SqlServer/QueryableValuesDbContextExtensions.cs +++ b/src/QueryableValues.SqlServer/QueryableValuesDbContextExtensions.cs @@ -1,5 +1,6 @@ #if EFCORE using BlazarTech.QueryableValues.Builders; +using BlazarTech.QueryableValues.SqlServer; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using System; @@ -44,7 +45,7 @@ private static void ValidateParameters(DbContext dbContext, IEnumerable va private static IQueryableFactory GetQueryableFactory(DbContext dbContext) { - return dbContext.GetService(); + return dbContext.GetService().Create(); } /// diff --git a/src/QueryableValues.SqlServer/QueryableValuesSqlServerExtension.cs b/src/QueryableValues.SqlServer/QueryableValuesSqlServerExtension.cs index a66f6f8..41ea08f 100644 --- a/src/QueryableValues.SqlServer/QueryableValuesSqlServerExtension.cs +++ b/src/QueryableValues.SqlServer/QueryableValuesSqlServerExtension.cs @@ -50,28 +50,9 @@ public void ApplyServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); - - services.AddScoped(sp => - { - var options = sp.GetRequiredService(); - var extension = options.FindExtension() ?? throw new InvalidOperationException(); - - switch (extension.Options.WithSerializationOptions) - { - case SerializationOptions.UseJson: - { - var serializer = sp.GetRequiredService(); - return new SqlServer.JsonQueryableFactory(serializer, extension.Options); - } - case SerializationOptions.UseXml: - { - var serializer = sp.GetRequiredService(); - return new SqlServer.XmlQueryableFactory(serializer, extension.Options); - } - default: - throw new NotImplementedException(); - } - }); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } public void Validate(IDbContextOptions options) @@ -95,11 +76,11 @@ public override void PopulateDebugInfo(IDictionary debugInfo) { } -#if EFCORE6 || EFCORE7 +#if EFCORE3 || EFCORE5 + public override long GetServiceProviderHashCode() => 0; +#else public override int GetServiceProviderHashCode() => 0; public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) => true; -#else - public override long GetServiceProviderHashCode() => 0; #endif } } diff --git a/src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs b/src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs index e03121c..a63aafc 100644 --- a/src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs +++ b/src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs @@ -2,6 +2,7 @@ using BlazarTech.QueryableValues.Builders; using BlazarTech.QueryableValues.Serializers; using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore.Infrastructure; using System; using System.Collections.Generic; using System.Data; @@ -10,8 +11,8 @@ namespace BlazarTech.QueryableValues.SqlServer { internal sealed class JsonQueryableFactory : QueryableFactory { - public JsonQueryableFactory(IJsonSerializer serializer, QueryableValuesSqlServerOptions options) - : base(serializer, options) + public JsonQueryableFactory(IJsonSerializer serializer, IDbContextOptions dbContextOptions) + : base(serializer, dbContextOptions) { } diff --git a/src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs b/src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs index df75f46..4310594 100644 --- a/src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs +++ b/src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs @@ -3,6 +3,7 @@ using BlazarTech.QueryableValues.Serializers; using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.ObjectPool; using System; using System.Collections.Concurrent; @@ -33,20 +34,22 @@ internal abstract class QueryableFactory : IQueryableFactory private readonly ISerializer _serializer; private readonly QueryableValuesSqlServerOptions _options; - public QueryableFactory(ISerializer serializer, QueryableValuesSqlServerOptions options) + public QueryableFactory(ISerializer serializer, IDbContextOptions dbContextOptions) { if (serializer is null) { throw new ArgumentNullException(nameof(serializer)); } - if (options is null) + if (dbContextOptions is null) { - throw new ArgumentNullException(nameof(options)); + throw new ArgumentNullException(nameof(dbContextOptions)); } + var extension = dbContextOptions.FindExtension() ?? throw new InvalidOperationException($"{nameof(QueryableValuesSqlServerExtension)} not found."); + _serializer = serializer; - _options = options; + _options = extension.Options; } /// diff --git a/src/QueryableValues.SqlServer/SqlServer/QueryableFactoryFactory.cs b/src/QueryableValues.SqlServer/SqlServer/QueryableFactoryFactory.cs new file mode 100644 index 0000000..4a53691 --- /dev/null +++ b/src/QueryableValues.SqlServer/SqlServer/QueryableFactoryFactory.cs @@ -0,0 +1,33 @@ +#if EFCORE +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using System; + +namespace BlazarTech.QueryableValues.SqlServer +{ + internal sealed class QueryableFactoryFactory + { + private readonly IServiceProvider _serviceProvider; + private readonly QueryableValuesSqlServerOptions _options; + + public QueryableFactoryFactory(IServiceProvider serviceProvider, IDbContextOptions dbContextOptions) + { + var extension = dbContextOptions.FindExtension() ?? throw new InvalidOperationException($"{nameof(QueryableValuesSqlServerExtension)} not found."); + _options = extension.Options; + _serviceProvider = serviceProvider; + } + + public IQueryableFactory Create() + { + if (_options.WithSerializationOptions == SerializationOptions.UseJson) + { + return _serviceProvider.GetRequiredService(); + } + else + { + return _serviceProvider.GetRequiredService(); + } + } + } +} +#endif diff --git a/src/QueryableValues.SqlServer/SqlServer/XmlQueryableFactory.cs b/src/QueryableValues.SqlServer/SqlServer/XmlQueryableFactory.cs index 5d65c34..7c13fde 100644 --- a/src/QueryableValues.SqlServer/SqlServer/XmlQueryableFactory.cs +++ b/src/QueryableValues.SqlServer/SqlServer/XmlQueryableFactory.cs @@ -2,6 +2,7 @@ using BlazarTech.QueryableValues.Builders; using BlazarTech.QueryableValues.Serializers; using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore.Infrastructure; using System; using System.Collections.Generic; using System.Data; @@ -10,8 +11,8 @@ namespace BlazarTech.QueryableValues.SqlServer { internal sealed class XmlQueryableFactory : QueryableFactory { - public XmlQueryableFactory(IXmlSerializer serializer, QueryableValuesSqlServerOptions options) - : base(serializer, options) + public XmlQueryableFactory(IXmlSerializer serializer, IDbContextOptions dbContextOptions) + : base(serializer, dbContextOptions) { } From 43fa9c1744b9c0b0cae66e84002416b399d7f93a Mon Sep 17 00:00:00 2001 From: yv989c Date: Tue, 21 Mar 2023 00:44:32 -0400 Subject: [PATCH 09/29] test: MustControlSelectTopOptimization compat --- .../Integration/InfrastructureTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs b/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs index d671ade..c444374 100644 --- a/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs +++ b/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs @@ -78,7 +78,7 @@ async Task isOptimizationEnabledSimpleType(MyDbContextBase db) var result = await db.AsQueryableValues(values).ToListAsync(); Assert.Equal(values.Length, result.Count); var logEntry = logEntries.Single(i => i.Contains("RelationalEventId.CommandExecuted")); - return Regex.IsMatch(logEntry, @"SELECT TOP\(@\w+\)\s+I.value\("); + return Regex.IsMatch(logEntry, @"SELECT TOP\(@\w+\)\s"); } async Task isOptimizationEnabledComplexType(MyDbContextBase db) @@ -94,7 +94,7 @@ async Task isOptimizationEnabledComplexType(MyDbContextBase db) var result = await db.AsQueryableValues(values).ToListAsync(); Assert.Equal(values.Length, result.Count); var logEntry = logEntries.Single(i => i.Contains("RelationalEventId.CommandExecuted")); - return Regex.IsMatch(logEntry, @"SELECT TOP\(@\w+\)\s+I.value\("); + return Regex.IsMatch(logEntry, @"SELECT TOP\(@\w+\)\s"); } } #endif From e7f82c2776efec498cb8f8a7c3c3cb0dee6d5ee5 Mon Sep 17 00:00:00 2001 From: yv989c Date: Tue, 21 Mar 2023 00:45:15 -0400 Subject: [PATCH 10/29] test: expose options --- .../Integration/MyDbContext.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/QueryableValues.SqlServer.Tests/Integration/MyDbContext.cs b/tests/QueryableValues.SqlServer.Tests/Integration/MyDbContext.cs index 82fcc3b..270566c 100644 --- a/tests/QueryableValues.SqlServer.Tests/Integration/MyDbContext.cs +++ b/tests/QueryableValues.SqlServer.Tests/Integration/MyDbContext.cs @@ -1,5 +1,6 @@ #if TESTS using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; namespace BlazarTech.QueryableValues.SqlServer.Tests.Integration { @@ -18,11 +19,17 @@ internal static class DatabaseName public class MyDbContext : MyDbContextBase, IMyDbContext { - public MyDbContext() : base(DatabaseName.Name) { } + public QueryableValuesSqlServerOptions Options { get; } + + public MyDbContext() : base(DatabaseName.Name) + { + Options = this.GetService().FindExtension()!.Options; + } } public interface IMyDbContext : IQueryableValuesEnabledDbContext { + QueryableValuesSqlServerOptions Options { get; } DbSet TestData { get; set; } } From 43300e15a736f3bdc0a20fa29521165f439af24f Mon Sep 17 00:00:00 2001 From: yv989c Date: Tue, 21 Mar 2023 00:46:59 -0400 Subject: [PATCH 11/29] test: json compat (wip) --- .../Integration/SimpleTypeTests.cs | 91 ++++++++++++++----- 1 file changed, 69 insertions(+), 22 deletions(-) diff --git a/tests/QueryableValues.SqlServer.Tests/Integration/SimpleTypeTests.cs b/tests/QueryableValues.SqlServer.Tests/Integration/SimpleTypeTests.cs index cbca5a4..6116756 100644 --- a/tests/QueryableValues.SqlServer.Tests/Integration/SimpleTypeTests.cs +++ b/tests/QueryableValues.SqlServer.Tests/Integration/SimpleTypeTests.cs @@ -16,6 +16,7 @@ public class SimpleTypeTests public SimpleTypeTests(DbContextFixture contextFixture) { _db = contextFixture.Db; + //_db.Options.Serialization(SerializationOptions.UseJson); } [Fact] @@ -209,16 +210,36 @@ public async Task MustMatchSequenceOfChar() { var values = new[] { 'A', 'a', 'ᴭ', ' ', '\n', '\0', '\u0001' }; + if (_db.Options.WithSerializationOptions == SerializationOptions.UseXml) { - var expected = new[] { 'A', 'a', '?', ' ', '\n', '?', '?' }; - var actual = await _db.AsQueryableValues(values, isUnicode: false).ToListAsync(); - Assert.Equal(expected, actual); + { + var expected = new[] { 'A', 'a', '?', ' ', '\n', '?', '?' }; + var actual = await _db.AsQueryableValues(values, isUnicode: false).ToListAsync(); + Assert.Equal(expected, actual); + } + + { + var expected = new[] { 'A', 'a', 'ᴭ', ' ', '\n', '?', '?' }; + var actual = await _db.AsQueryableValues(values, isUnicode: true).ToListAsync(); + Assert.Equal(expected, actual); + } } + else if (_db.Options.WithSerializationOptions == SerializationOptions.UseJson) + { + { + var expected = new[] { 'A', 'a', '?', ' ', '\n', '\0', '\u0001' }; + var actual = await _db.AsQueryableValues(values, isUnicode: false).ToListAsync(); + Assert.Equal(expected, actual); + } + { + var actual = await _db.AsQueryableValues(values, isUnicode: true).ToListAsync(); + Assert.Equal(values, actual); + } + } + else { - var expected = new[] { 'A', 'a', 'ᴭ', ' ', '\n', '?', '?' }; - var actual = await _db.AsQueryableValues(values, isUnicode: true).ToListAsync(); - Assert.Equal(expected, actual); + throw new NotImplementedException(); } { @@ -233,29 +254,55 @@ public async Task MustMatchSequenceOfString() { var values = new[] { "\0 ", "\u0001", "Test 1", "Test <2>", "Test &3", "😀", "ᴭ", "", " ", "\n", " \n", " \n ", "\r", "\r ", " Test\t1 ", "\U00010330" }; + if (_db.Options.WithSerializationOptions == SerializationOptions.UseXml) { - var expected = new string[values.Length]; - values.CopyTo(expected, 0); - expected[0] = "? "; - expected[1] = "?"; - expected[5] = "??"; - expected[6] = "?"; - expected[15] = "??"; + { + var expected = new string[values.Length]; + values.CopyTo(expected, 0); + expected[0] = "? "; + expected[1] = "?"; + expected[5] = "??"; + expected[6] = "?"; + expected[15] = "??"; - var actual = await _db.AsQueryableValues(values, isUnicode: false).ToListAsync(); + var actual = await _db.AsQueryableValues(values, isUnicode: false).ToListAsync(); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } + + { + var expected = new string[values.Length]; + values.CopyTo(expected, 0); + expected[0] = "? "; + expected[1] = "?"; + var actual = await _db.AsQueryableValues(values, isUnicode: true).ToListAsync(); + + Assert.Equal(expected, actual); + } + } + else if (_db.Options.WithSerializationOptions == SerializationOptions.UseJson) { - var expected = new string[values.Length]; - values.CopyTo(expected, 0); - expected[0] = "? "; - expected[1] = "?"; + { + var expected = new string[values.Length]; + values.CopyTo(expected, 0); + expected[5] = "??"; + expected[6] = "?"; + expected[15] = "??"; - var actual = await _db.AsQueryableValues(values, isUnicode: true).ToListAsync(); + var actual = await _db.AsQueryableValues(values, isUnicode: false).ToListAsync(); - Assert.Equal(expected, actual); + Assert.Equal(expected, actual); + } + + { + var actual = await _db.AsQueryableValues(values, isUnicode: true).ToListAsync(); + Assert.Equal(values, actual); + } + } + else + { + throw new NotImplementedException(); } } From 37398cd7ec58e337beea7c2060d21bd9d4c4a41c Mon Sep 17 00:00:00 2001 From: yv989c Date: Tue, 21 Mar 2023 23:36:27 -0400 Subject: [PATCH 12/29] refactor: simplify sql format --- .../SqlServer/JsonQueryableFactory.cs | 8 +++----- .../SqlServer/XmlQueryableFactory.cs | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs b/src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs index a63aafc..83ac747 100644 --- a/src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs +++ b/src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs @@ -41,7 +41,7 @@ private string GetSqlForSimpleTypes(string sqlType, DeferredValues deferre var sqlTypeArguments = precisionScale.HasValue ? $"({precisionScale.Value.Precision},{precisionScale.Value.Scale})" : null; sql = - $"{sqlPrefix} V " + + $"{sqlPrefix} [V] " + $"FROM OPENJSON({{0}}) WITH ([V] {sqlType}{sqlTypeArguments} '$')"; SqlCache.TryAdd(cacheKey, sql); @@ -138,7 +138,6 @@ protected override string GetSqlForComplexTypes(IEntityOptionsBuilder entityOpti sb.AppendLine(); sb.Append("FROM OPENJSON({0}) WITH ("); - sb.AppendLine(); for (var i = 0; i < mappings.Count; i++) { @@ -147,12 +146,12 @@ protected override string GetSqlForComplexTypes(IEntityOptionsBuilder entityOpti if (i > 0) { - sb.Append(',').AppendLine(); + sb.Append(", "); } var targetName = mapping.Target.Name; - sb.Append("\t[").Append(targetName).Append("] "); + sb.Append('[').Append(targetName).Append("] "); switch (mapping.TypeName) { @@ -217,7 +216,6 @@ protected override string GetSqlForComplexTypes(IEntityOptionsBuilder entityOpti } } - sb.AppendLine(); sb.Append(')'); return sb.ToString(); diff --git a/src/QueryableValues.SqlServer/SqlServer/XmlQueryableFactory.cs b/src/QueryableValues.SqlServer/SqlServer/XmlQueryableFactory.cs index 7c13fde..df829d0 100644 --- a/src/QueryableValues.SqlServer/SqlServer/XmlQueryableFactory.cs +++ b/src/QueryableValues.SqlServer/SqlServer/XmlQueryableFactory.cs @@ -42,7 +42,7 @@ private string GetSqlForSimpleTypes(string xmlType, string sqlType, DeferredV var sqlTypeArguments = precisionScale.HasValue ? $"({precisionScale.Value.Precision},{precisionScale.Value.Scale})" : null; sql = - $"{sqlPrefix} I.value('. cast as xs:{xmlType}?', '{sqlType}{sqlTypeArguments}') AS V " + + $"{sqlPrefix} I.value('. cast as xs:{xmlType}?', '{sqlType}{sqlTypeArguments}') [V] " + "FROM {0}.nodes('/R/V') N(I)"; SqlCache.TryAdd(cacheKey, sql); From e406d4042fe5fb0265c25ca85bbaad21976ae74b Mon Sep 17 00:00:00 2001 From: yv989c Date: Wed, 22 Mar 2023 00:13:44 -0400 Subject: [PATCH 13/29] fix: sql cache --- .../SqlServer/JsonQueryableFactory.cs | 4 +++- .../SqlServer/QueryableFactory.cs | 15 ++++++++++++++- .../SqlServer/XmlQueryableFactory.cs | 4 +++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs b/src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs index 83ac747..c543f16 100644 --- a/src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs +++ b/src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs @@ -25,13 +25,15 @@ private string GetSqlForSimpleTypes(string sqlType, DeferredValues deferre where T : notnull { var useSelectTopOptimization = UseSelectTopOptimization(deferredValues); - var cacheKey = new + var cacheKeyProperties = new { SqlType = sqlType, UseSelectTopOptimization = useSelectTopOptimization, PrecisionScale = precisionScale }; + var cacheKey = GetCacheKey(cacheKeyProperties); + if (SqlCache.TryGetValue(cacheKey, out string? sql)) { return sql; diff --git a/src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs b/src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs index 4310594..76fc6e1 100644 --- a/src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs +++ b/src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs @@ -33,6 +33,7 @@ internal abstract class QueryableFactory : IQueryableFactory private readonly ISerializer _serializer; private readonly QueryableValuesSqlServerOptions _options; + private readonly string _cacheScopeName; public QueryableFactory(ISerializer serializer, IDbContextOptions dbContextOptions) { @@ -50,6 +51,16 @@ public QueryableFactory(ISerializer serializer, IDbContextOptions dbContextOptio _serializer = serializer; _options = extension.Options; + _cacheScopeName = GetType().Name ?? throw new InvalidOperationException(); + } + + protected object GetCacheKey(object properties) + { + return new + { + Scope = _cacheScopeName, + Properties = properties + }; } /// @@ -264,12 +275,14 @@ string getSql(IReadOnlyList mappings, Action(); } - var cacheKey = new + var cacheKeyProperties = new { Options = entityOptions, UseSelectTopOptimization = useSelectTopOptimization }; + var cacheKey = GetCacheKey(cacheKeyProperties); + if (SqlCache.TryGetValue(cacheKey, out string? sqlFromCache)) { return sqlFromCache; diff --git a/src/QueryableValues.SqlServer/SqlServer/XmlQueryableFactory.cs b/src/QueryableValues.SqlServer/SqlServer/XmlQueryableFactory.cs index df829d0..36e85cd 100644 --- a/src/QueryableValues.SqlServer/SqlServer/XmlQueryableFactory.cs +++ b/src/QueryableValues.SqlServer/SqlServer/XmlQueryableFactory.cs @@ -25,7 +25,7 @@ private string GetSqlForSimpleTypes(string xmlType, string sqlType, DeferredV where T : notnull { var useSelectTopOptimization = UseSelectTopOptimization(deferredValues); - var cacheKey = new + var cacheKeyProperties = new { XmlType = xmlType, SqlType = sqlType, @@ -33,6 +33,8 @@ private string GetSqlForSimpleTypes(string xmlType, string sqlType, DeferredV PrecisionScale = precisionScale }; + var cacheKey = GetCacheKey(cacheKeyProperties); + if (SqlCache.TryGetValue(cacheKey, out string? sql)) { return sql; From a1264f2da5fc985af168a6ecdca9bae1e69ab790 Mon Sep 17 00:00:00 2001 From: yv989c Date: Wed, 22 Mar 2023 00:43:01 -0400 Subject: [PATCH 14/29] refactor: consolidate configuration validation --- .../QueryableValuesDbContextExtensions.cs | 28 ++++++++----------- .../SqlServer/QueryableFactoryFactory.cs | 3 +- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/src/QueryableValues.SqlServer/QueryableValuesDbContextExtensions.cs b/src/QueryableValues.SqlServer/QueryableValuesDbContextExtensions.cs index 86cc049..c24e6dc 100644 --- a/src/QueryableValues.SqlServer/QueryableValuesDbContextExtensions.cs +++ b/src/QueryableValues.SqlServer/QueryableValuesDbContextExtensions.cs @@ -14,20 +14,6 @@ namespace BlazarTech.QueryableValues /// public static class QueryableValuesDbContextExtensions { - private static void EnsureConfigured(DbContext dbContext) - { - var options = dbContext.GetService(); - var extension = options.FindExtension(); - - if (extension is null) - { - var message = $"{nameof(QueryableValues)} have not been configured for {dbContext.GetType().Name}. " + - "More info: https://github.com/yv989c/BlazarTech.QueryableValues#configuration"; - - throw new InvalidOperationException(message); - } - } - private static void ValidateParameters(DbContext dbContext, IEnumerable values) { if (dbContext is null) @@ -39,13 +25,21 @@ private static void ValidateParameters(DbContext dbContext, IEnumerable va { throw new ArgumentNullException(nameof(values)); } - - EnsureConfigured(dbContext); } private static IQueryableFactory GetQueryableFactory(DbContext dbContext) { - return dbContext.GetService().Create(); + try + { + return dbContext.GetService()?.Create() ?? throw new InvalidOperationException(); + } + catch (InvalidOperationException) + { + var message = $"{nameof(QueryableValues)} have not been configured for {dbContext.GetType().Name}. " + + "More info: https://github.com/yv989c/BlazarTech.QueryableValues#configuration"; + + throw new InvalidOperationException(message); + } } /// diff --git a/src/QueryableValues.SqlServer/SqlServer/QueryableFactoryFactory.cs b/src/QueryableValues.SqlServer/SqlServer/QueryableFactoryFactory.cs index 4a53691..7625242 100644 --- a/src/QueryableValues.SqlServer/SqlServer/QueryableFactoryFactory.cs +++ b/src/QueryableValues.SqlServer/SqlServer/QueryableFactoryFactory.cs @@ -12,9 +12,8 @@ internal sealed class QueryableFactoryFactory public QueryableFactoryFactory(IServiceProvider serviceProvider, IDbContextOptions dbContextOptions) { - var extension = dbContextOptions.FindExtension() ?? throw new InvalidOperationException($"{nameof(QueryableValuesSqlServerExtension)} not found."); - _options = extension.Options; _serviceProvider = serviceProvider; + _options = (dbContextOptions.FindExtension()?.Options) ?? throw new InvalidOperationException(); } public IQueryableFactory Create() From c6a734b72ff02a886accfb14e7338c299f9f53df Mon Sep 17 00:00:00 2001 From: yv989c Date: Wed, 22 Mar 2023 00:46:18 -0400 Subject: [PATCH 15/29] test: covers json scenarios and QueryableFactoryFactory --- .../Integration/ComplexTypeTests.cs | 22 +++++++++- .../Integration/InfrastructureTests.cs | 43 +++++++++++++++++-- .../Integration/MyDbContext.cs | 6 +-- .../Integration/SimpleTypeTests.cs | 24 +++++++++-- 4 files changed, 82 insertions(+), 13 deletions(-) diff --git a/tests/QueryableValues.SqlServer.Tests/Integration/ComplexTypeTests.cs b/tests/QueryableValues.SqlServer.Tests/Integration/ComplexTypeTests.cs index 0b7ac33..c6f21b0 100644 --- a/tests/QueryableValues.SqlServer.Tests/Integration/ComplexTypeTests.cs +++ b/tests/QueryableValues.SqlServer.Tests/Integration/ComplexTypeTests.cs @@ -9,9 +9,9 @@ namespace BlazarTech.QueryableValues.SqlServer.Tests.Integration { [Collection("DbContext")] - public class ComplexTypeTests + public abstract class ComplexTypeTests { - private readonly IMyDbContext _db; + protected readonly IMyDbContext _db; public class TestType { @@ -796,5 +796,23 @@ public async Task MustMatchCount() Assert.Equal(expectedItemCount, actualItemCount); } } + + [Collection("DbContext")] + public class JsonComplexTypeTests : ComplexTypeTests + { + public JsonComplexTypeTests(DbContextFixture contextFixture) : base(contextFixture) + { + _db.Options.Serialization(SerializationOptions.UseJson); + } + } + + [Collection("DbContext")] + public class XmlComplexTypeTests : ComplexTypeTests + { + public XmlComplexTypeTests(DbContextFixture contextFixture) : base(contextFixture) + { + _db.Options.Serialization(SerializationOptions.UseXml); + } + } } #endif \ No newline at end of file diff --git a/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs b/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs index c444374..a7b7056 100644 --- a/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs +++ b/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs @@ -1,5 +1,6 @@ #if TESTS && TEST_ALL using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; @@ -54,19 +55,23 @@ public void OnlyWorksOnDbContext() } #if !EFCORE3 - [Fact] - public async Task MustControlSelectTopOptimization() + [Theory] + [InlineData(SerializationOptions.UseJson)] + [InlineData(SerializationOptions.UseXml)] + public async Task MustControlSelectTopOptimization(SerializationOptions serializationOptions) { var services = new ServiceCollection(); services.AddDbContext(); - services.AddDbContext(); + services.AddDbContext(); using var serviceProvider = services.BuildServiceProvider(); var optimizedDb = serviceProvider.GetRequiredService(); + optimizedDb.Options.Serialization(serializationOptions); Assert.True(await isOptimizationEnabledSimpleType(optimizedDb)); Assert.True(await isOptimizationEnabledComplexType(optimizedDb)); - var notOptimizedDb = serviceProvider.GetRequiredService(); + var notOptimizedDb = serviceProvider.GetRequiredService(); + notOptimizedDb.Options.Serialization(serializationOptions); Assert.False(await isOptimizationEnabledComplexType(notOptimizedDb)); Assert.False(await isOptimizationEnabledSimpleType(notOptimizedDb)); @@ -98,6 +103,36 @@ async Task isOptimizationEnabledComplexType(MyDbContextBase db) } } #endif + + [Theory] + [InlineData(SerializationOptions.UseJson)] + [InlineData(SerializationOptions.UseXml)] + public void MustCreateQueryableFactory(SerializationOptions serializationOptions) + { + var services = new ServiceCollection(); + services.AddDbContext(); + + using var serviceProvider = services.BuildServiceProvider(); + + var dbContext = serviceProvider.GetRequiredService(); + dbContext.Options.Serialization(serializationOptions); + + var queryableFactory = dbContext.GetService().Create(); + + Assert.NotNull(queryableFactory); + + switch (serializationOptions) + { + case SerializationOptions.UseJson: + Assert.IsType(queryableFactory); + break; + case SerializationOptions.UseXml: + Assert.IsType(queryableFactory); + break; + default: + throw new NotImplementedException(); + } + } } class NotADbContext : IQueryableValuesEnabledDbContext diff --git a/tests/QueryableValues.SqlServer.Tests/Integration/MyDbContext.cs b/tests/QueryableValues.SqlServer.Tests/Integration/MyDbContext.cs index 270566c..0dc5e2b 100644 --- a/tests/QueryableValues.SqlServer.Tests/Integration/MyDbContext.cs +++ b/tests/QueryableValues.SqlServer.Tests/Integration/MyDbContext.cs @@ -21,7 +21,7 @@ public class MyDbContext : MyDbContextBase, IMyDbContext { public QueryableValuesSqlServerOptions Options { get; } - public MyDbContext() : base(DatabaseName.Name) + public MyDbContext(bool useSelectTopOptimization = true) : base(DatabaseName.Name, useSelectTopOptimization: useSelectTopOptimization) { Options = this.GetService().FindExtension()!.Options; } @@ -38,9 +38,9 @@ public class NotConfiguredDbContext : MyDbContextBase public NotConfiguredDbContext() : base(DatabaseName.Name, useQueryableValues: false) { } } - public class NotOptimizedDbContext : MyDbContextBase + public class NotOptimizedMyDbContext : MyDbContext { - public NotOptimizedDbContext() : base(DatabaseName.Name, useSelectTopOptimization: false) { } + public NotOptimizedMyDbContext() : base(useSelectTopOptimization: false) { } } } #endif \ No newline at end of file diff --git a/tests/QueryableValues.SqlServer.Tests/Integration/SimpleTypeTests.cs b/tests/QueryableValues.SqlServer.Tests/Integration/SimpleTypeTests.cs index 6116756..dcd8571 100644 --- a/tests/QueryableValues.SqlServer.Tests/Integration/SimpleTypeTests.cs +++ b/tests/QueryableValues.SqlServer.Tests/Integration/SimpleTypeTests.cs @@ -8,15 +8,13 @@ namespace BlazarTech.QueryableValues.SqlServer.Tests.Integration { - [Collection("DbContext")] - public class SimpleTypeTests + public abstract class SimpleTypeTests { - private readonly IMyDbContext _db; + protected readonly IMyDbContext _db; public SimpleTypeTests(DbContextFixture contextFixture) { _db = contextFixture.Db; - //_db.Options.Serialization(SerializationOptions.UseJson); } [Fact] @@ -731,5 +729,23 @@ async Task AssertCount(Func getValue) } } } + + [Collection("DbContext")] + public class JsonSimpleTypeTests : SimpleTypeTests + { + public JsonSimpleTypeTests(DbContextFixture contextFixture) : base(contextFixture) + { + _db.Options.Serialization(SerializationOptions.UseJson); + } + } + + [Collection("DbContext")] + public class XmlSimpleTypeTests : SimpleTypeTests + { + public XmlSimpleTypeTests(DbContextFixture contextFixture) : base(contextFixture) + { + _db.Options.Serialization(SerializationOptions.UseXml); + } + } } #endif \ No newline at end of file From f46fe9322be4d5763250540eb968d6767f611a10 Mon Sep 17 00:00:00 2001 From: yv989c Date: Thu, 23 Mar 2023 00:57:37 -0400 Subject: [PATCH 16/29] feat: implements JSON support auto-detection --- .../QueryableValuesDbContextExtensions.cs | 2 +- .../QueryableValuesSqlServerExtension.cs | 2 + .../QueryableValuesSqlServerOptions.cs | 6 +- .../SerializationOptions.cs | 8 ++ .../JsonSupportConnectionInterceptor.cs | 102 ++++++++++++++++++ .../SqlServer/QueryableFactoryFactory.cs | 13 ++- .../Integration/InfrastructureTests.cs | 4 +- 7 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 src/QueryableValues.SqlServer/SqlServer/JsonSupportConnectionInterceptor.cs diff --git a/src/QueryableValues.SqlServer/QueryableValuesDbContextExtensions.cs b/src/QueryableValues.SqlServer/QueryableValuesDbContextExtensions.cs index c24e6dc..ce49e9d 100644 --- a/src/QueryableValues.SqlServer/QueryableValuesDbContextExtensions.cs +++ b/src/QueryableValues.SqlServer/QueryableValuesDbContextExtensions.cs @@ -31,7 +31,7 @@ private static IQueryableFactory GetQueryableFactory(DbContext dbContext) { try { - return dbContext.GetService()?.Create() ?? throw new InvalidOperationException(); + return dbContext.GetService()?.Create(dbContext) ?? throw new InvalidOperationException(); } catch (InvalidOperationException) { diff --git a/src/QueryableValues.SqlServer/QueryableValuesSqlServerExtension.cs b/src/QueryableValues.SqlServer/QueryableValuesSqlServerExtension.cs index 41ea08f..e8e970b 100644 --- a/src/QueryableValues.SqlServer/QueryableValuesSqlServerExtension.cs +++ b/src/QueryableValues.SqlServer/QueryableValuesSqlServerExtension.cs @@ -1,4 +1,5 @@ #if EFCORE +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.DependencyInjection; using System; @@ -53,6 +54,7 @@ public void ApplyServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } public void Validate(IDbContextOptions options) diff --git a/src/QueryableValues.SqlServer/QueryableValuesSqlServerOptions.cs b/src/QueryableValues.SqlServer/QueryableValuesSqlServerOptions.cs index 6b3bf3c..56b33c8 100644 --- a/src/QueryableValues.SqlServer/QueryableValuesSqlServerOptions.cs +++ b/src/QueryableValues.SqlServer/QueryableValuesSqlServerOptions.cs @@ -10,7 +10,7 @@ public sealed class QueryableValuesSqlServerOptions { internal bool WithUseSelectTopOptimization { get; private set; } = true; internal bool WithUseDeferredEnumeration { get; private set; } = true; - internal SerializationOptions WithSerializationOptions { get; private set; } = SerializationOptions.UseXml; + internal SerializationOptions WithSerializationOptions { get; private set; } = SerializationOptions.Auto; /// /// When possible, uses a TOP(n) clause in the underlying SELECT statement to assist SQL Server memory grant estimation. The default is . @@ -28,11 +28,11 @@ public QueryableValuesSqlServerOptions UseSelectTopOptimization(bool useSelectTo } /// - /// Configures serialization options. The default is . + /// Configures serialization options. The default is . /// /// Serialization options. /// The same instance so subsequent configurations can be chained. - public QueryableValuesSqlServerOptions Serialization(SerializationOptions options = SerializationOptions.UseXml) + public QueryableValuesSqlServerOptions Serialization(SerializationOptions options = SerializationOptions.Auto) { if (Enum.IsDefined(typeof(SerializationOptions), options)) { diff --git a/src/QueryableValues.SqlServer/SerializationOptions.cs b/src/QueryableValues.SqlServer/SerializationOptions.cs index d553303..d8a0a43 100644 --- a/src/QueryableValues.SqlServer/SerializationOptions.cs +++ b/src/QueryableValues.SqlServer/SerializationOptions.cs @@ -5,6 +5,14 @@ /// public enum SerializationOptions { + /// + /// Use the JSON serializer if possible; otherwise, it will use XML. + /// + /// + /// This option causes an additional roundtrip to the database to check if JSON serialization is supported. This only happens once per connection string uniqueness. + /// + Auto = 0, + /// /// Use the JSON serializer. /// diff --git a/src/QueryableValues.SqlServer/SqlServer/JsonSupportConnectionInterceptor.cs b/src/QueryableValues.SqlServer/SqlServer/JsonSupportConnectionInterceptor.cs new file mode 100644 index 0000000..d4a4f23 --- /dev/null +++ b/src/QueryableValues.SqlServer/SqlServer/JsonSupportConnectionInterceptor.cs @@ -0,0 +1,102 @@ +#if EFCORE +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Concurrent; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +namespace BlazarTech.QueryableValues.SqlServer +{ + sealed class JsonSupportConnectionInterceptor : DbConnectionInterceptor + { + private static readonly ConcurrentDictionary ConnectionStringJsonSupport = new(); + + private readonly ILogger _logger; + + public JsonSupportConnectionInterceptor(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(new DbLoggerCategory.Database.Command()); + } + + private static string GetKey(DbConnection connection) + { + return connection.ConnectionString; + } + + public static bool HasJsonSupport(DbContext dbContext) + { + var connection = dbContext.Database.GetDbConnection(); + + if (ConnectionStringJsonSupport.TryGetValue(GetKey(connection), out var hasJsonSupport)) + { + return hasJsonSupport; + } + + return false; + } + + private static bool MustDetect(DbConnection connection) + { + return !ConnectionStringJsonSupport.ContainsKey(GetKey(connection)); + } + + public override async Task ConnectionOpenedAsync(DbConnection connection, ConnectionEndEventData eventData, CancellationToken cancellationToken = default) + { + if (connection is SqlConnection sqlConnection && MustDetect(sqlConnection)) + { + await DetectJsonSupportAsync(sqlConnection).ConfigureAwait(false); + } + } + + public override void ConnectionOpened(DbConnection connection, ConnectionEndEventData eventData) + { + if (connection is SqlConnection sqlConnection && MustDetect(sqlConnection)) + { + DetectJsonSupportAsync(sqlConnection) + .ConfigureAwait(false) + .GetAwaiter() + .GetResult(); + } + } + + private async ValueTask DetectJsonSupportAsync(SqlConnection connection) + { + var hasJsonSupport = false; + + try + { + var majorVersionNumber = getMajorVersionNumber(connection.ServerVersion); + + // https://learn.microsoft.com/en-us/sql/t-sql/functions/openjson-transact-sql + if (majorVersionNumber >= 13) + { + using var cm = new SqlCommand("SELECT [compatibility_level] FROM [sys].[databases] WHERE [database_id] = DB_ID()", connection); + var compatibilityLevelObject = await cm.ExecuteScalarAsync().ConfigureAwait(false); + var compatibilityLevel = Convert.ToInt32(compatibilityLevelObject); + hasJsonSupport = compatibilityLevel >= 130; + } + } + catch (Exception ex) + { + _logger.LogError(ex, ""); + } + + ConnectionStringJsonSupport.TryAdd(GetKey(connection), hasJsonSupport); + + static int getMajorVersionNumber(string? serverVersion) + { + if (Version.TryParse(serverVersion, out var version)) + { + return version.Major; + } + + return 0; + } + } + } +} +#endif \ No newline at end of file diff --git a/src/QueryableValues.SqlServer/SqlServer/QueryableFactoryFactory.cs b/src/QueryableValues.SqlServer/SqlServer/QueryableFactoryFactory.cs index 7625242..ce98b91 100644 --- a/src/QueryableValues.SqlServer/SqlServer/QueryableFactoryFactory.cs +++ b/src/QueryableValues.SqlServer/SqlServer/QueryableFactoryFactory.cs @@ -1,4 +1,5 @@ #if EFCORE +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.DependencyInjection; using System; @@ -16,9 +17,17 @@ public QueryableFactoryFactory(IServiceProvider serviceProvider, IDbContextOptio _options = (dbContextOptions.FindExtension()?.Options) ?? throw new InvalidOperationException(); } - public IQueryableFactory Create() + public IQueryableFactory Create(DbContext dbContext) { - if (_options.WithSerializationOptions == SerializationOptions.UseJson) + var useJson = _options.WithSerializationOptions switch + { + SerializationOptions.Auto => JsonSupportConnectionInterceptor.HasJsonSupport(dbContext), + SerializationOptions.UseJson => true, + SerializationOptions.UseXml => false, + _ => throw new NotImplementedException(), + }; + + if (useJson) { return _serviceProvider.GetRequiredService(); } diff --git a/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs b/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs index a7b7056..bb9cf30 100644 --- a/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs +++ b/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs @@ -107,6 +107,7 @@ async Task isOptimizationEnabledComplexType(MyDbContextBase db) [Theory] [InlineData(SerializationOptions.UseJson)] [InlineData(SerializationOptions.UseXml)] + [InlineData(SerializationOptions.Auto)] public void MustCreateQueryableFactory(SerializationOptions serializationOptions) { var services = new ServiceCollection(); @@ -117,13 +118,14 @@ public void MustCreateQueryableFactory(SerializationOptions serializationOptions var dbContext = serviceProvider.GetRequiredService(); dbContext.Options.Serialization(serializationOptions); - var queryableFactory = dbContext.GetService().Create(); + var queryableFactory = dbContext.GetService().Create(dbContext); Assert.NotNull(queryableFactory); switch (serializationOptions) { case SerializationOptions.UseJson: + case SerializationOptions.Auto: Assert.IsType(queryableFactory); break; case SerializationOptions.UseXml: From 7909a1a8b28f606729430c4c23c38a487e9c3820 Mon Sep 17 00:00:00 2001 From: yv989c Date: Thu, 23 Mar 2023 23:09:32 -0400 Subject: [PATCH 17/29] test: serialization format control --- .../Integration/InfrastructureTests.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs b/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs index bb9cf30..c8d3535 100644 --- a/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs +++ b/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs @@ -102,6 +102,63 @@ async Task isOptimizationEnabledComplexType(MyDbContextBase db) return Regex.IsMatch(logEntry, @"SELECT TOP\(@\w+\)\s"); } } + + [Theory] + [InlineData(SerializationOptions.UseJson)] + [InlineData(SerializationOptions.UseXml)] + public async Task MustControlSerializationFormat(SerializationOptions serializationOptions) + { + var services = new ServiceCollection(); + services.AddDbContext(); + + using var serviceProvider = services.BuildServiceProvider(); + + var db = serviceProvider.GetRequiredService(); + db.Options.Serialization(serializationOptions); + + Assert.True(await isRightSerializationFormatSimpleType(db)); + Assert.True(await isRightSerializationFormatComplexType(db)); + + async Task isRightSerializationFormatSimpleType(MyDbContextBase db) + { + var values = new[] { 1, 2, 3 }; + var logEntries = new List(); + db.LogEntryEmitted += logEntry => logEntries.Add(logEntry); + var result = await db.AsQueryableValues(values).ToListAsync(); + Assert.Equal(values.Length, result.Count); + var logEntry = logEntries.Single(i => i.Contains("RelationalEventId.CommandExecuted")); + return isRightFormat(logEntry); + } + + async Task isRightSerializationFormatComplexType(MyDbContextBase db) + { + var values = new[] + { + new { Id = 1 }, + new { Id = 2 }, + new { Id = 3 } + }; + var logEntries = new List(); + db.LogEntryEmitted += logEntry => logEntries.Add(logEntry); + var result = await db.AsQueryableValues(values).ToListAsync(); + Assert.Equal(values.Length, result.Count); + var logEntry = logEntries.Single(i => i.Contains("RelationalEventId.CommandExecuted")); + return isRightFormat(logEntry); + } + + bool isRightFormat(string logEntry) + { + switch (serializationOptions) + { + case SerializationOptions.UseJson: + return logEntry.Contains("OPENJSON("); + case SerializationOptions.UseXml: + return logEntry.Contains(".nodes("); + default: + throw new NotImplementedException(); + } + } + } #endif [Theory] From ddf3a91090a8b2afb42cab385c05ac29b6e5267d Mon Sep 17 00:00:00 2001 From: yv989c Date: Thu, 23 Mar 2023 23:10:17 -0400 Subject: [PATCH 18/29] test: json support detection --- .../Integration/InfrastructureTests.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs b/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs index c8d3535..b6f4322 100644 --- a/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs +++ b/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs @@ -159,6 +159,32 @@ bool isRightFormat(string logEntry) } } } + + [Fact] + public async Task MustDetectJsonSupport() + { + var services = new ServiceCollection(); + services.AddDbContext(); + + using var serviceProvider = services.BuildServiceProvider(); + + var db = serviceProvider.GetRequiredService(); + + forceJsonDetection(); + Assert.False(JsonSupportConnectionInterceptor.HasJsonSupport(db)); + await db.TestData.FirstAsync(); + Assert.True(JsonSupportConnectionInterceptor.HasJsonSupport(db)); + + forceJsonDetection(); + Assert.False(JsonSupportConnectionInterceptor.HasJsonSupport(db)); + db.TestData.First(); + Assert.True(JsonSupportConnectionInterceptor.HasJsonSupport(db)); + + void forceJsonDetection() + { + db.Database.SetConnectionString(db.Database.GetConnectionString() + ";"); + } + } #endif [Theory] From 733d631bf5c2f3e83279af3c466f4ca57374e79f Mon Sep 17 00:00:00 2001 From: yv989c Date: Sun, 26 Mar 2023 00:54:52 -0400 Subject: [PATCH 19/29] test: json support detection --- .../Integration/InfrastructureTests.cs | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs b/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs index b6f4322..6dbd7ed 100644 --- a/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs +++ b/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs @@ -160,8 +160,11 @@ bool isRightFormat(string logEntry) } } - [Fact] - public async Task MustDetectJsonSupport() + [Theory] + [InlineData(SerializationOptions.Auto)] + [InlineData(SerializationOptions.UseJson)] + [InlineData(SerializationOptions.UseXml)] + public async Task JsonSupportDetection(SerializationOptions serializationOptions) { var services = new ServiceCollection(); services.AddDbContext(); @@ -169,20 +172,39 @@ public async Task MustDetectJsonSupport() using var serviceProvider = services.BuildServiceProvider(); var db = serviceProvider.GetRequiredService(); + db.Options.Serialization(serializationOptions); - forceJsonDetection(); - Assert.False(JsonSupportConnectionInterceptor.HasJsonSupport(db)); - await db.TestData.FirstAsync(); - Assert.True(JsonSupportConnectionInterceptor.HasJsonSupport(db)); - - forceJsonDetection(); - Assert.False(JsonSupportConnectionInterceptor.HasJsonSupport(db)); - db.TestData.First(); - Assert.True(JsonSupportConnectionInterceptor.HasJsonSupport(db)); + switch (serializationOptions) + { + case SerializationOptions.Auto: + { + forceJsonDetection(); + Assert.Null(JsonSupportConnectionInterceptor.HasJsonSupport(db)); + await db.TestData.FirstAsync(); + Assert.True(JsonSupportConnectionInterceptor.HasJsonSupport(db)); + + forceJsonDetection(); + Assert.Null(JsonSupportConnectionInterceptor.HasJsonSupport(db)); + db.TestData.First(); + Assert.True(JsonSupportConnectionInterceptor.HasJsonSupport(db)); + } + break; + case SerializationOptions.UseJson: + case SerializationOptions.UseXml: + { + forceJsonDetection(); + Assert.Null(JsonSupportConnectionInterceptor.HasJsonSupport(db)); + db.TestData.First(); + Assert.Null(JsonSupportConnectionInterceptor.HasJsonSupport(db)); + } + break; + default: + throw new NotImplementedException(); + } void forceJsonDetection() { - db.Database.SetConnectionString(db.Database.GetConnectionString() + ";"); + db.Database.SetConnectionString(db.Database.GetConnectionString() + $";Application Name={Guid.NewGuid()};"); } } #endif From 4d3dd3ae919f67309378351343793078698ceffe Mon Sep 17 00:00:00 2001 From: yv989c Date: Sun, 26 Mar 2023 01:02:46 -0400 Subject: [PATCH 20/29] feat: json query optimization Steers SQL Server (16.0.1000.6) to avoid executing the OPENJSON operation more than once on some scenarios. --- .../SqlServer/JsonQueryableFactory.cs | 4 ++-- src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs b/src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs index c543f16..6028f5a 100644 --- a/src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs +++ b/src/QueryableValues.SqlServer/SqlServer/JsonQueryableFactory.cs @@ -44,7 +44,7 @@ private string GetSqlForSimpleTypes(string sqlType, DeferredValues deferre sql = $"{sqlPrefix} [V] " + - $"FROM OPENJSON({{0}}) WITH ([V] {sqlType}{sqlTypeArguments} '$')"; + $"FROM OPENJSON({{0}}) WITH ([V] {sqlType}{sqlTypeArguments} '$', [_] BIT '$._') ORDER BY [_]"; SqlCache.TryAdd(cacheKey, sql); @@ -218,7 +218,7 @@ protected override string GetSqlForComplexTypes(IEntityOptionsBuilder entityOpti } } - sb.Append(')'); + sb.Append(", [_] BIT '$._') ORDER BY [_]"); return sb.ToString(); } diff --git a/src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs b/src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs index 76fc6e1..0a355e0 100644 --- a/src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs +++ b/src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs @@ -18,7 +18,7 @@ namespace BlazarTech.QueryableValues.SqlServer { internal abstract class QueryableFactory : IQueryableFactory { - protected const string SqlSelect = "SELECT"; + protected const string SqlSelect = "SELECT TOP(2147483647)"; protected const string SqlSelectTop = "SELECT TOP({1})"; protected static readonly ConcurrentDictionary SqlCache = new(); From 7cb25251ba5e1cf71a83560a23f8963db7e3e1c1 Mon Sep 17 00:00:00 2001 From: yv989c Date: Sun, 26 Mar 2023 01:07:38 -0400 Subject: [PATCH 21/29] refactor: encapsulate the options --- .../QueryableValuesSqlServerExtension.cs | 1 + .../SqlServer/ExtensionOptions.cs | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 src/QueryableValues.SqlServer/SqlServer/ExtensionOptions.cs diff --git a/src/QueryableValues.SqlServer/QueryableValuesSqlServerExtension.cs b/src/QueryableValues.SqlServer/QueryableValuesSqlServerExtension.cs index e8e970b..132a330 100644 --- a/src/QueryableValues.SqlServer/QueryableValuesSqlServerExtension.cs +++ b/src/QueryableValues.SqlServer/QueryableValuesSqlServerExtension.cs @@ -53,6 +53,7 @@ public void ApplyServices(IServiceCollection services) services.AddSingleton(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); } diff --git a/src/QueryableValues.SqlServer/SqlServer/ExtensionOptions.cs b/src/QueryableValues.SqlServer/SqlServer/ExtensionOptions.cs new file mode 100644 index 0000000..e3401b6 --- /dev/null +++ b/src/QueryableValues.SqlServer/SqlServer/ExtensionOptions.cs @@ -0,0 +1,17 @@ +#if EFCORE +using Microsoft.EntityFrameworkCore.Infrastructure; +using System; + +namespace BlazarTech.QueryableValues.SqlServer +{ + internal sealed class ExtensionOptions + { + public QueryableValuesSqlServerOptions Options { get; } + + public ExtensionOptions(IDbContextOptions dbContextOptions) + { + Options = (dbContextOptions.FindExtension()?.Options) ?? throw new InvalidOperationException(); + } + } +} +#endif From f5e394b1d4f014e582970280337e5a5927b87834 Mon Sep 17 00:00:00 2001 From: yv989c Date: Sun, 26 Mar 2023 01:10:34 -0400 Subject: [PATCH 22/29] fix: do json support detection only in auto mode --- .../SqlServer/JsonSupportConnectionInterceptor.cs | 14 +++++++++----- .../SqlServer/QueryableFactoryFactory.cs | 7 +++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/QueryableValues.SqlServer/SqlServer/JsonSupportConnectionInterceptor.cs b/src/QueryableValues.SqlServer/SqlServer/JsonSupportConnectionInterceptor.cs index d4a4f23..c8a2346 100644 --- a/src/QueryableValues.SqlServer/SqlServer/JsonSupportConnectionInterceptor.cs +++ b/src/QueryableValues.SqlServer/SqlServer/JsonSupportConnectionInterceptor.cs @@ -16,10 +16,12 @@ sealed class JsonSupportConnectionInterceptor : DbConnectionInterceptor private static readonly ConcurrentDictionary ConnectionStringJsonSupport = new(); private readonly ILogger _logger; + private readonly QueryableValuesSqlServerOptions _options; - public JsonSupportConnectionInterceptor(ILoggerFactory loggerFactory) + public JsonSupportConnectionInterceptor(ILoggerFactory loggerFactory, ExtensionOptions extensionOptions) { _logger = loggerFactory.CreateLogger(new DbLoggerCategory.Database.Command()); + _options = extensionOptions.Options; } private static string GetKey(DbConnection connection) @@ -27,7 +29,7 @@ private static string GetKey(DbConnection connection) return connection.ConnectionString; } - public static bool HasJsonSupport(DbContext dbContext) + public static bool? HasJsonSupport(DbContext dbContext) { var connection = dbContext.Database.GetDbConnection(); @@ -36,12 +38,14 @@ public static bool HasJsonSupport(DbContext dbContext) return hasJsonSupport; } - return false; + return null; } - private static bool MustDetect(DbConnection connection) + private bool MustDetect(DbConnection connection) { - return !ConnectionStringJsonSupport.ContainsKey(GetKey(connection)); + return + _options.WithSerializationOptions == SerializationOptions.Auto && + !ConnectionStringJsonSupport.ContainsKey(GetKey(connection)); } public override async Task ConnectionOpenedAsync(DbConnection connection, ConnectionEndEventData eventData, CancellationToken cancellationToken = default) diff --git a/src/QueryableValues.SqlServer/SqlServer/QueryableFactoryFactory.cs b/src/QueryableValues.SqlServer/SqlServer/QueryableFactoryFactory.cs index ce98b91..2a1dd3c 100644 --- a/src/QueryableValues.SqlServer/SqlServer/QueryableFactoryFactory.cs +++ b/src/QueryableValues.SqlServer/SqlServer/QueryableFactoryFactory.cs @@ -1,6 +1,5 @@ #if EFCORE using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.DependencyInjection; using System; @@ -11,17 +10,17 @@ internal sealed class QueryableFactoryFactory private readonly IServiceProvider _serviceProvider; private readonly QueryableValuesSqlServerOptions _options; - public QueryableFactoryFactory(IServiceProvider serviceProvider, IDbContextOptions dbContextOptions) + public QueryableFactoryFactory(IServiceProvider serviceProvider, ExtensionOptions extensionOptions) { _serviceProvider = serviceProvider; - _options = (dbContextOptions.FindExtension()?.Options) ?? throw new InvalidOperationException(); + _options = extensionOptions.Options; } public IQueryableFactory Create(DbContext dbContext) { var useJson = _options.WithSerializationOptions switch { - SerializationOptions.Auto => JsonSupportConnectionInterceptor.HasJsonSupport(dbContext), + SerializationOptions.Auto => JsonSupportConnectionInterceptor.HasJsonSupport(dbContext).GetValueOrDefault(), SerializationOptions.UseJson => true, SerializationOptions.UseXml => false, _ => throw new NotImplementedException(), From b7fc494677e36e82139515539ab06978b13447be Mon Sep 17 00:00:00 2001 From: yv989c Date: Sun, 26 Mar 2023 01:47:46 -0400 Subject: [PATCH 23/29] chore: benchmarks without vs. with-xml vs. with-json --- BlazarTech.QueryableValues.sln | 8 + .../ContainsBenchmarks.cs | 184 ++++++++++++++---- .../MyDbContext.cs | 25 ++- .../Program.cs | 10 +- ...ueryableValues.SqlServer.Benchmarks.csproj | 9 +- 5 files changed, 187 insertions(+), 49 deletions(-) diff --git a/BlazarTech.QueryableValues.sln b/BlazarTech.QueryableValues.sln index 72466ac..eb19b89 100644 --- a/BlazarTech.QueryableValues.sln +++ b/BlazarTech.QueryableValues.sln @@ -30,6 +30,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QueryableValues.SqlServer.E EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QueryableValues.SqlServer.Tests.EFCore7", "tests\QueryableValues.SqlServer.Tests.EFCore7\QueryableValues.SqlServer.Tests.EFCore7.csproj", "{D5293D2F-4649-4A2B-A50D-E7DBD4AA4A5C}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QueryableValues.SqlServer.Benchmarks", "benchmarks\QueryableValues.SqlServer.Benchmarks\QueryableValues.SqlServer.Benchmarks.csproj", "{99FE31A0-BC7E-412C-82E2-DA19E8B68613}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -110,6 +112,12 @@ Global {D5293D2F-4649-4A2B-A50D-E7DBD4AA4A5C}.Test_All|Any CPU.Build.0 = Test_All|Any CPU {D5293D2F-4649-4A2B-A50D-E7DBD4AA4A5C}.Test|Any CPU.ActiveCfg = Test|Any CPU {D5293D2F-4649-4A2B-A50D-E7DBD4AA4A5C}.Test|Any CPU.Build.0 = Test|Any CPU + {99FE31A0-BC7E-412C-82E2-DA19E8B68613}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {99FE31A0-BC7E-412C-82E2-DA19E8B68613}.Release|Any CPU.ActiveCfg = Release|Any CPU + {99FE31A0-BC7E-412C-82E2-DA19E8B68613}.Release|Any CPU.Build.0 = Release|Any CPU + {99FE31A0-BC7E-412C-82E2-DA19E8B68613}.Test_All|Any CPU.ActiveCfg = Release|Any CPU + {99FE31A0-BC7E-412C-82E2-DA19E8B68613}.Test_All|Any CPU.Build.0 = Release|Any CPU + {99FE31A0-BC7E-412C-82E2-DA19E8B68613}.Test|Any CPU.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/benchmarks/QueryableValues.SqlServer.Benchmarks/ContainsBenchmarks.cs b/benchmarks/QueryableValues.SqlServer.Benchmarks/ContainsBenchmarks.cs index 20b0a2b..e080793 100644 --- a/benchmarks/QueryableValues.SqlServer.Benchmarks/ContainsBenchmarks.cs +++ b/benchmarks/QueryableValues.SqlServer.Benchmarks/ContainsBenchmarks.cs @@ -4,22 +4,43 @@ using BlazarTech.QueryableValues; using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; +using System.Text; namespace QueryableValues.SqlServer.Benchmarks; -[SimpleJob(RunStrategy.Monitoring, warmupCount: 1, targetCount: 25, invocationCount: 200)] +//[SimpleJob(RunStrategy.Monitoring, warmupCount: 1, iterationCount: 25, invocationCount: 200)] +//[SimpleJob(RunStrategy.Monitoring, warmupCount: 1, iterationCount: 6, invocationCount: 200)] +[SimpleJob(RunStrategy.Monitoring, warmupCount: 1, iterationCount: 6, invocationCount: 32)] [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] [GcServer(true), MemoryDiagnoser] public class ContainsBenchmarks { -#pragma warning disable CS8618 - private IQueryable _int32Query; - private IQueryable _guidQuery; - private IQueryable _queryableValuesInt32Query; - private IQueryable _queryableValuesGuidQuery; -#pragma warning restore CS8618 - - [Params(2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096)] + private IQueryable _int32Query = default!; + private IQueryable _guidQuery = default!; + private IQueryable _stringQuery = default!; + + private IQueryable _queryableValuesJsonInt32Query = default!; + private IQueryable _queryableValuesJsonGuidQuery = default!; + private IQueryable _queryableValuesJsonStringQuery = default!; + + private IQueryable _queryableValuesXmlInt32Query = default!; + private IQueryable _queryableValuesXmlGuidQuery = default!; + private IQueryable _queryableValuesXmlStringQuery = default!; + + public enum DataType + { + Int32, + Guid, + String + } + + [Params(DataType.Int32, DataType.Guid, DataType.String)] + //[Params(DataType.String)] + public DataType Type { get; set; } + + //[Params(512)] + //[Params(2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096)] + [Params(2, 8, 32, 128, 512, 2048)] public int NumberOfValues { get; set; } private IEnumerable GetIntValues() @@ -38,26 +59,59 @@ private IEnumerable GetGuidValues() } } + private IEnumerable GetStringValues() + { + var sb = new StringBuilder(); + + for (int i = 0; i < NumberOfValues; i++) + { + sb.Clear(); + var length = Random.Shared.Next(0, 50); + for (int x = 0; x < length; x++) + { + sb.Append((char)Random.Shared.Next(32, 126)); + } + yield return sb.ToString(); + } + } + [GlobalSetup] public void GlobalSetup() { Console.WriteLine("Initializing..."); - var dbContext = new MyDbContext(); + var dbContextXml = new MyDbContext(SerializationOptions.UseXml); + var dbContextJson = new MyDbContext(SerializationOptions.UseJson); #region Init db { - var wasCreated = dbContext.Database.EnsureCreated(); + var wasCreated = dbContextXml.Database.EnsureCreated(); if (wasCreated) { - for (int i = 0; i < 1000; i++) + const int itemsCount = 1000; + + for (int i = 0; i < itemsCount; i++) + { + dbContextXml.Add(new Int32Entity()); + dbContextXml.Add(new GuidEntity()); + } + + var i2 = 0; + + foreach (var value in GetStringValues()) { - dbContext.Add(new Int32Entity()); - dbContext.Add(new GuidEntity()); + i2++; + + dbContextXml.Add(new StringEntity { Id = $"{value}{i2}" }); + + if (i2 == itemsCount) + { + break; + } } - dbContext.SaveChanges(); + dbContextXml.SaveChanges(); } var versionParam = new SqlParameter("@Version", System.Data.SqlDbType.NVarChar, -1) @@ -65,11 +119,11 @@ public void GlobalSetup() Direction = System.Data.ParameterDirection.Output }; - dbContext.Database.ExecuteSqlRaw("SET @Version = @@VERSION;", versionParam); + dbContextXml.Database.ExecuteSqlRaw("SET @Version = @@VERSION;", versionParam); Console.WriteLine(versionParam.Value); - dbContext.Database.ExecuteSqlRaw("DBCC FREEPROCCACHE; DBCC DROPCLEANBUFFERS;"); + dbContextXml.Database.ExecuteSqlRaw("DBCC FREEPROCCACHE; DBCC DROPCLEANBUFFERS;"); } #endregion @@ -77,11 +131,14 @@ public void GlobalSetup() { var intValues = GetIntValues(); - _int32Query = dbContext.Int32Entities + _int32Query = dbContextXml.Int32Entities .Where(i => intValues.Contains(i.Id)); - _queryableValuesInt32Query = dbContext.Int32Entities - .Where(i => dbContext.AsQueryableValues(intValues).Contains(i.Id)); + _queryableValuesXmlInt32Query = dbContextXml.Int32Entities + .Where(i => dbContextXml.AsQueryableValues(intValues).Contains(i.Id)); + + _queryableValuesJsonInt32Query = dbContextJson.Int32Entities + .Where(i => dbContextJson.AsQueryableValues(intValues).Contains(i.Id)); } #endregion @@ -89,36 +146,87 @@ public void GlobalSetup() { var guidValues = GetGuidValues(); - _guidQuery = dbContext.GuidEntities + _guidQuery = dbContextXml.GuidEntities .Where(i => guidValues.Contains(i.Id)); - _queryableValuesGuidQuery = dbContext.GuidEntities - .Where(i => dbContext.AsQueryableValues(guidValues).Contains(i.Id)); + _queryableValuesXmlGuidQuery = dbContextXml.GuidEntities + .Where(i => dbContextXml.AsQueryableValues(guidValues).Contains(i.Id)); + + _queryableValuesJsonGuidQuery = dbContextJson.GuidEntities + .Where(i => dbContextJson.AsQueryableValues(guidValues).Contains(i.Id)); } #endregion - } - [Benchmark(Baseline = true), BenchmarkCategory("Int32")] - public void Without_Int32() - { - _int32Query.Any(); + #region String Queries + { + var stringValues = GetStringValues(); + + _stringQuery = dbContextXml.StringEntities + .Where(i => stringValues.Contains(i.Id)); + + _queryableValuesXmlStringQuery = dbContextXml.StringEntities + .Where(i => dbContextXml.AsQueryableValues(stringValues, true).Contains(i.Id)); + + _queryableValuesJsonStringQuery = dbContextJson.StringEntities + .Where(i => dbContextJson.AsQueryableValues(stringValues, true).Contains(i.Id)); + } + #endregion } - [Benchmark, BenchmarkCategory("Int32")] - public void With_Int32() + [Benchmark(Baseline = true)] + public void Without() { - _queryableValuesInt32Query.Any(); + switch (Type) + { + case DataType.Int32: + _int32Query.Any(); + break; + case DataType.Guid: + _guidQuery.Any(); + break; + case DataType.String: + _stringQuery.Any(); + break; + default: + throw new NotImplementedException(); + } } - [Benchmark(Baseline = true), BenchmarkCategory("Guid")] - public void Without_Guid() + [Benchmark] + public void WithXml() { - _guidQuery.Any(); + switch (Type) + { + case DataType.Int32: + _queryableValuesXmlInt32Query.Any(); + break; + case DataType.Guid: + _queryableValuesXmlGuidQuery.Any(); + break; + case DataType.String: + _queryableValuesXmlStringQuery.Any(); + break; + default: + throw new NotImplementedException(); + } } - [Benchmark, BenchmarkCategory("Guid")] - public void With_Guid() + [Benchmark] + public void WithJson() { - _queryableValuesGuidQuery.Any(); + switch (Type) + { + case DataType.Int32: + _queryableValuesJsonInt32Query.Any(); + break; + case DataType.Guid: + _queryableValuesJsonGuidQuery.Any(); + break; + case DataType.String: + _queryableValuesJsonStringQuery.Any(); + break; + default: + throw new NotImplementedException(); + } } -} +} \ No newline at end of file diff --git a/benchmarks/QueryableValues.SqlServer.Benchmarks/MyDbContext.cs b/benchmarks/QueryableValues.SqlServer.Benchmarks/MyDbContext.cs index 074201f..aadcaf4 100644 --- a/benchmarks/QueryableValues.SqlServer.Benchmarks/MyDbContext.cs +++ b/benchmarks/QueryableValues.SqlServer.Benchmarks/MyDbContext.cs @@ -1,20 +1,27 @@ using BlazarTech.QueryableValues; using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; namespace QueryableValues.SqlServer.Benchmarks { public class MyDbContext : DbContext { -#pragma warning disable CS8618 - public DbSet Int32Entities { get; set; } - public DbSet GuidEntities { get; set; } -#pragma warning restore CS8618 + private readonly SerializationOptions _serializationOptions; + + public DbSet Int32Entities { get; set; } = default!; + public DbSet GuidEntities { get; set; } = default!; + public DbSet StringEntities { get; set; } = default!; + + public MyDbContext(SerializationOptions serializationOptions) + { + _serializationOptions = serializationOptions; + } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer( - @"Server=.\SQLEXPRESS;Integrated Security=true;Database=QueryableValuesBenchmarks", - builder => builder.UseQueryableValues() + @"Server=.\SQLEXPRESS;Integrated Security=true;Database=QueryableValuesBenchmarks;Encrypt=False;", + builder => builder.UseQueryableValues(options => options.Serialization(_serializationOptions)) ); } @@ -33,4 +40,10 @@ public class GuidEntity { public Guid Id { get; set; } } + + public class StringEntity + { + [MaxLength(100)] + public string Id { get; set; } = default!; + } } diff --git a/benchmarks/QueryableValues.SqlServer.Benchmarks/Program.cs b/benchmarks/QueryableValues.SqlServer.Benchmarks/Program.cs index 64d8299..ae4fb51 100644 --- a/benchmarks/QueryableValues.SqlServer.Benchmarks/Program.cs +++ b/benchmarks/QueryableValues.SqlServer.Benchmarks/Program.cs @@ -1,4 +1,5 @@ -using BenchmarkDotNet.Running; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Running; namespace QueryableValues.SqlServer.Benchmarks; @@ -6,6 +7,11 @@ class Program { static void Main(string[] args) { - var summary = BenchmarkRunner.Run(); + var config = new ManualConfig(); + + config.Add(DefaultConfig.Instance); + config.WithOptions(ConfigOptions.DisableOptimizationsValidator); + + BenchmarkRunner.Run(config); } } \ No newline at end of file diff --git a/benchmarks/QueryableValues.SqlServer.Benchmarks/QueryableValues.SqlServer.Benchmarks.csproj b/benchmarks/QueryableValues.SqlServer.Benchmarks/QueryableValues.SqlServer.Benchmarks.csproj index d713035..40380b1 100644 --- a/benchmarks/QueryableValues.SqlServer.Benchmarks/QueryableValues.SqlServer.Benchmarks.csproj +++ b/benchmarks/QueryableValues.SqlServer.Benchmarks/QueryableValues.SqlServer.Benchmarks.csproj @@ -8,9 +8,12 @@ - - - + + + + + + From 4e77b3521bb0fc2a6801a8e8eb931753681da480 Mon Sep 17 00:00:00 2001 From: yv989c Date: Sun, 26 Mar 2023 12:47:50 -0400 Subject: [PATCH 24/29] chore: benchmark final parameters --- .../ContainsBenchmarks.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/benchmarks/QueryableValues.SqlServer.Benchmarks/ContainsBenchmarks.cs b/benchmarks/QueryableValues.SqlServer.Benchmarks/ContainsBenchmarks.cs index e080793..d674d7f 100644 --- a/benchmarks/QueryableValues.SqlServer.Benchmarks/ContainsBenchmarks.cs +++ b/benchmarks/QueryableValues.SqlServer.Benchmarks/ContainsBenchmarks.cs @@ -8,9 +8,7 @@ namespace QueryableValues.SqlServer.Benchmarks; -//[SimpleJob(RunStrategy.Monitoring, warmupCount: 1, iterationCount: 25, invocationCount: 200)] -//[SimpleJob(RunStrategy.Monitoring, warmupCount: 1, iterationCount: 6, invocationCount: 200)] -[SimpleJob(RunStrategy.Monitoring, warmupCount: 1, iterationCount: 6, invocationCount: 32)] +[SimpleJob(RunStrategy.Monitoring, warmupCount: 1, iterationCount: 25, invocationCount: 200)] [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] [GcServer(true), MemoryDiagnoser] public class ContainsBenchmarks From bad114332b15ed0cad6b33cc8f313c39053fb2d5 Mon Sep 17 00:00:00 2001 From: yv989c Date: Sun, 26 Mar 2023 13:40:49 -0400 Subject: [PATCH 25/29] refactor: rename SerializationOptions to SqlServerSerialization and docs --- .../ContainsBenchmarks.cs | 4 +- .../MyDbContext.cs | 8 +-- .../QueryableValuesSqlServerOptions.cs | 14 ++--- .../SerializationOptions.cs | 41 ------------- .../JsonSupportConnectionInterceptor.cs | 2 +- .../SqlServer/QueryableFactoryFactory.cs | 8 +-- .../SqlServerSerialization.cs | 43 +++++++++++++ .../Integration/ComplexTypeTests.cs | 4 +- .../Integration/InfrastructureTests.cs | 60 +++++++++---------- .../Integration/SimpleTypeTests.cs | 12 ++-- 10 files changed, 99 insertions(+), 97 deletions(-) delete mode 100644 src/QueryableValues.SqlServer/SerializationOptions.cs create mode 100644 src/QueryableValues.SqlServer/SqlServerSerialization.cs diff --git a/benchmarks/QueryableValues.SqlServer.Benchmarks/ContainsBenchmarks.cs b/benchmarks/QueryableValues.SqlServer.Benchmarks/ContainsBenchmarks.cs index d674d7f..7296a47 100644 --- a/benchmarks/QueryableValues.SqlServer.Benchmarks/ContainsBenchmarks.cs +++ b/benchmarks/QueryableValues.SqlServer.Benchmarks/ContainsBenchmarks.cs @@ -78,8 +78,8 @@ public void GlobalSetup() { Console.WriteLine("Initializing..."); - var dbContextXml = new MyDbContext(SerializationOptions.UseXml); - var dbContextJson = new MyDbContext(SerializationOptions.UseJson); + var dbContextXml = new MyDbContext(SqlServerSerialization.UseXml); + var dbContextJson = new MyDbContext(SqlServerSerialization.UseJson); #region Init db { diff --git a/benchmarks/QueryableValues.SqlServer.Benchmarks/MyDbContext.cs b/benchmarks/QueryableValues.SqlServer.Benchmarks/MyDbContext.cs index aadcaf4..6fdc15e 100644 --- a/benchmarks/QueryableValues.SqlServer.Benchmarks/MyDbContext.cs +++ b/benchmarks/QueryableValues.SqlServer.Benchmarks/MyDbContext.cs @@ -6,22 +6,22 @@ namespace QueryableValues.SqlServer.Benchmarks { public class MyDbContext : DbContext { - private readonly SerializationOptions _serializationOptions; + private readonly SqlServerSerialization _serializationOption; public DbSet Int32Entities { get; set; } = default!; public DbSet GuidEntities { get; set; } = default!; public DbSet StringEntities { get; set; } = default!; - public MyDbContext(SerializationOptions serializationOptions) + public MyDbContext(SqlServerSerialization serializationOption) { - _serializationOptions = serializationOptions; + _serializationOption = serializationOption; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer( @"Server=.\SQLEXPRESS;Integrated Security=true;Database=QueryableValuesBenchmarks;Encrypt=False;", - builder => builder.UseQueryableValues(options => options.Serialization(_serializationOptions)) + builder => builder.UseQueryableValues(options => options.Serialization(_serializationOption)) ); } diff --git a/src/QueryableValues.SqlServer/QueryableValuesSqlServerOptions.cs b/src/QueryableValues.SqlServer/QueryableValuesSqlServerOptions.cs index 56b33c8..1acee93 100644 --- a/src/QueryableValues.SqlServer/QueryableValuesSqlServerOptions.cs +++ b/src/QueryableValues.SqlServer/QueryableValuesSqlServerOptions.cs @@ -10,7 +10,7 @@ public sealed class QueryableValuesSqlServerOptions { internal bool WithUseSelectTopOptimization { get; private set; } = true; internal bool WithUseDeferredEnumeration { get; private set; } = true; - internal SerializationOptions WithSerializationOptions { get; private set; } = SerializationOptions.Auto; + internal SqlServerSerialization WithSerializationOption { get; private set; } = SqlServerSerialization.Auto; /// /// When possible, uses a TOP(n) clause in the underlying SELECT statement to assist SQL Server memory grant estimation. The default is . @@ -28,19 +28,19 @@ public QueryableValuesSqlServerOptions UseSelectTopOptimization(bool useSelectTo } /// - /// Configures serialization options. The default is . + /// Configures the serialization format to be used when sending data to SQL Server. The default is . /// - /// Serialization options. + /// Serialization options. /// The same instance so subsequent configurations can be chained. - public QueryableValuesSqlServerOptions Serialization(SerializationOptions options = SerializationOptions.Auto) + public QueryableValuesSqlServerOptions Serialization(SqlServerSerialization option = SqlServerSerialization.Auto) { - if (Enum.IsDefined(typeof(SerializationOptions), options)) + if (Enum.IsDefined(typeof(SqlServerSerialization), option)) { - WithSerializationOptions = options; + WithSerializationOption = option; } else { - throw new ArgumentOutOfRangeException(nameof(options)); + throw new ArgumentOutOfRangeException(nameof(option)); } return this; diff --git a/src/QueryableValues.SqlServer/SerializationOptions.cs b/src/QueryableValues.SqlServer/SerializationOptions.cs deleted file mode 100644 index d8a0a43..0000000 --- a/src/QueryableValues.SqlServer/SerializationOptions.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace BlazarTech.QueryableValues -{ - /// - /// Serialization options. - /// - public enum SerializationOptions - { - /// - /// Use the JSON serializer if possible; otherwise, it will use XML. - /// - /// - /// This option causes an additional roundtrip to the database to check if JSON serialization is supported. This only happens once per connection string uniqueness. - /// - Auto = 0, - - /// - /// Use the JSON serializer. - /// - /// - /// - /// In my tests, JSON significantly outperforms XML, particularly on big sequences. - ///
- ///
- /// JSON can only be used when the following is true:
- /// - The SQL Server instance is 2016 and above.
- /// - The database has a compatibility level of 130 or higher. - ///
- ///
- /// More info: - ///
- ///
- /// WARNING: An error will occur at runtime if JSON serialization is not supported. - ///
- UseJson = 1, - - /// - /// Use the XML serializer. - /// - UseXml = 2 - } -} diff --git a/src/QueryableValues.SqlServer/SqlServer/JsonSupportConnectionInterceptor.cs b/src/QueryableValues.SqlServer/SqlServer/JsonSupportConnectionInterceptor.cs index c8a2346..d2aec15 100644 --- a/src/QueryableValues.SqlServer/SqlServer/JsonSupportConnectionInterceptor.cs +++ b/src/QueryableValues.SqlServer/SqlServer/JsonSupportConnectionInterceptor.cs @@ -44,7 +44,7 @@ private static string GetKey(DbConnection connection) private bool MustDetect(DbConnection connection) { return - _options.WithSerializationOptions == SerializationOptions.Auto && + _options.WithSerializationOption == SqlServerSerialization.Auto && !ConnectionStringJsonSupport.ContainsKey(GetKey(connection)); } diff --git a/src/QueryableValues.SqlServer/SqlServer/QueryableFactoryFactory.cs b/src/QueryableValues.SqlServer/SqlServer/QueryableFactoryFactory.cs index 2a1dd3c..811c944 100644 --- a/src/QueryableValues.SqlServer/SqlServer/QueryableFactoryFactory.cs +++ b/src/QueryableValues.SqlServer/SqlServer/QueryableFactoryFactory.cs @@ -18,11 +18,11 @@ public QueryableFactoryFactory(IServiceProvider serviceProvider, ExtensionOption public IQueryableFactory Create(DbContext dbContext) { - var useJson = _options.WithSerializationOptions switch + var useJson = _options.WithSerializationOption switch { - SerializationOptions.Auto => JsonSupportConnectionInterceptor.HasJsonSupport(dbContext).GetValueOrDefault(), - SerializationOptions.UseJson => true, - SerializationOptions.UseXml => false, + SqlServerSerialization.Auto => JsonSupportConnectionInterceptor.HasJsonSupport(dbContext).GetValueOrDefault(), + SqlServerSerialization.UseJson => true, + SqlServerSerialization.UseXml => false, _ => throw new NotImplementedException(), }; diff --git a/src/QueryableValues.SqlServer/SqlServerSerialization.cs b/src/QueryableValues.SqlServer/SqlServerSerialization.cs new file mode 100644 index 0000000..fbbb0c6 --- /dev/null +++ b/src/QueryableValues.SqlServer/SqlServerSerialization.cs @@ -0,0 +1,43 @@ +#if EFCORE +namespace BlazarTech.QueryableValues +{ + /// + /// Specifies the serialization format to be used when sending data to SQL Server. + /// + public enum SqlServerSerialization + { + /// + /// Automatically chooses between JSON and XML serialization based on server and database compatibility. + /// + /// + /// + /// This option may cause an additional round-trip to the database to check for JSON compatibility, + /// but only once per unique connection string for the life of the process. If JSON serialization is not supported, XML is used instead. + /// + /// + /// Caveat: If the very first query sent to the server is a QueryableValues enabled one, it will use XML and then switch to JSON (if supported) afterward. + /// + /// + Auto = 0, + + /// + /// Uses the JSON serializer for better performance. + /// + /// + /// + /// Using JSON is faster than XML, but requires SQL Server 2016 or newer and a database compatibility level of 130 or higher.
+ /// More info: . + ///
+ /// + /// WARNING: If JSON serialization is not supported, an error will occur at runtime. + /// + ///
+ UseJson = 1, + + /// + /// Uses the XML serializer, which is compatible with all supported versions of SQL Server to date. + /// + UseXml = 2 + } +} +#endif \ No newline at end of file diff --git a/tests/QueryableValues.SqlServer.Tests/Integration/ComplexTypeTests.cs b/tests/QueryableValues.SqlServer.Tests/Integration/ComplexTypeTests.cs index c6f21b0..93552ac 100644 --- a/tests/QueryableValues.SqlServer.Tests/Integration/ComplexTypeTests.cs +++ b/tests/QueryableValues.SqlServer.Tests/Integration/ComplexTypeTests.cs @@ -802,7 +802,7 @@ public class JsonComplexTypeTests : ComplexTypeTests { public JsonComplexTypeTests(DbContextFixture contextFixture) : base(contextFixture) { - _db.Options.Serialization(SerializationOptions.UseJson); + _db.Options.Serialization(SqlServerSerialization.UseJson); } } @@ -811,7 +811,7 @@ public class XmlComplexTypeTests : ComplexTypeTests { public XmlComplexTypeTests(DbContextFixture contextFixture) : base(contextFixture) { - _db.Options.Serialization(SerializationOptions.UseXml); + _db.Options.Serialization(SqlServerSerialization.UseXml); } } } diff --git a/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs b/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs index 6dbd7ed..02a1a8b 100644 --- a/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs +++ b/tests/QueryableValues.SqlServer.Tests/Integration/InfrastructureTests.cs @@ -56,9 +56,9 @@ public void OnlyWorksOnDbContext() #if !EFCORE3 [Theory] - [InlineData(SerializationOptions.UseJson)] - [InlineData(SerializationOptions.UseXml)] - public async Task MustControlSelectTopOptimization(SerializationOptions serializationOptions) + [InlineData(SqlServerSerialization.UseJson)] + [InlineData(SqlServerSerialization.UseXml)] + public async Task MustControlSelectTopOptimization(SqlServerSerialization serializationOption) { var services = new ServiceCollection(); services.AddDbContext(); @@ -66,12 +66,12 @@ public async Task MustControlSelectTopOptimization(SerializationOptions serializ using var serviceProvider = services.BuildServiceProvider(); var optimizedDb = serviceProvider.GetRequiredService(); - optimizedDb.Options.Serialization(serializationOptions); + optimizedDb.Options.Serialization(serializationOption); Assert.True(await isOptimizationEnabledSimpleType(optimizedDb)); Assert.True(await isOptimizationEnabledComplexType(optimizedDb)); var notOptimizedDb = serviceProvider.GetRequiredService(); - notOptimizedDb.Options.Serialization(serializationOptions); + notOptimizedDb.Options.Serialization(serializationOption); Assert.False(await isOptimizationEnabledComplexType(notOptimizedDb)); Assert.False(await isOptimizationEnabledSimpleType(notOptimizedDb)); @@ -104,9 +104,9 @@ async Task isOptimizationEnabledComplexType(MyDbContextBase db) } [Theory] - [InlineData(SerializationOptions.UseJson)] - [InlineData(SerializationOptions.UseXml)] - public async Task MustControlSerializationFormat(SerializationOptions serializationOptions) + [InlineData(SqlServerSerialization.UseJson)] + [InlineData(SqlServerSerialization.UseXml)] + public async Task MustControlSerializationFormat(SqlServerSerialization serializationOption) { var services = new ServiceCollection(); services.AddDbContext(); @@ -114,7 +114,7 @@ public async Task MustControlSerializationFormat(SerializationOptions serializat using var serviceProvider = services.BuildServiceProvider(); var db = serviceProvider.GetRequiredService(); - db.Options.Serialization(serializationOptions); + db.Options.Serialization(serializationOption); Assert.True(await isRightSerializationFormatSimpleType(db)); Assert.True(await isRightSerializationFormatComplexType(db)); @@ -148,11 +148,11 @@ async Task isRightSerializationFormatComplexType(MyDbContextBase db) bool isRightFormat(string logEntry) { - switch (serializationOptions) + switch (serializationOption) { - case SerializationOptions.UseJson: + case SqlServerSerialization.UseJson: return logEntry.Contains("OPENJSON("); - case SerializationOptions.UseXml: + case SqlServerSerialization.UseXml: return logEntry.Contains(".nodes("); default: throw new NotImplementedException(); @@ -161,10 +161,10 @@ bool isRightFormat(string logEntry) } [Theory] - [InlineData(SerializationOptions.Auto)] - [InlineData(SerializationOptions.UseJson)] - [InlineData(SerializationOptions.UseXml)] - public async Task JsonSupportDetection(SerializationOptions serializationOptions) + [InlineData(SqlServerSerialization.Auto)] + [InlineData(SqlServerSerialization.UseJson)] + [InlineData(SqlServerSerialization.UseXml)] + public async Task JsonSupportDetection(SqlServerSerialization serializationOption) { var services = new ServiceCollection(); services.AddDbContext(); @@ -172,11 +172,11 @@ public async Task JsonSupportDetection(SerializationOptions serializationOptions using var serviceProvider = services.BuildServiceProvider(); var db = serviceProvider.GetRequiredService(); - db.Options.Serialization(serializationOptions); + db.Options.Serialization(serializationOption); - switch (serializationOptions) + switch (serializationOption) { - case SerializationOptions.Auto: + case SqlServerSerialization.Auto: { forceJsonDetection(); Assert.Null(JsonSupportConnectionInterceptor.HasJsonSupport(db)); @@ -189,8 +189,8 @@ public async Task JsonSupportDetection(SerializationOptions serializationOptions Assert.True(JsonSupportConnectionInterceptor.HasJsonSupport(db)); } break; - case SerializationOptions.UseJson: - case SerializationOptions.UseXml: + case SqlServerSerialization.UseJson: + case SqlServerSerialization.UseXml: { forceJsonDetection(); Assert.Null(JsonSupportConnectionInterceptor.HasJsonSupport(db)); @@ -210,10 +210,10 @@ void forceJsonDetection() #endif [Theory] - [InlineData(SerializationOptions.UseJson)] - [InlineData(SerializationOptions.UseXml)] - [InlineData(SerializationOptions.Auto)] - public void MustCreateQueryableFactory(SerializationOptions serializationOptions) + [InlineData(SqlServerSerialization.UseJson)] + [InlineData(SqlServerSerialization.UseXml)] + [InlineData(SqlServerSerialization.Auto)] + public void MustCreateQueryableFactory(SqlServerSerialization serializationOption) { var services = new ServiceCollection(); services.AddDbContext(); @@ -221,19 +221,19 @@ public void MustCreateQueryableFactory(SerializationOptions serializationOptions using var serviceProvider = services.BuildServiceProvider(); var dbContext = serviceProvider.GetRequiredService(); - dbContext.Options.Serialization(serializationOptions); + dbContext.Options.Serialization(serializationOption); var queryableFactory = dbContext.GetService().Create(dbContext); Assert.NotNull(queryableFactory); - switch (serializationOptions) + switch (serializationOption) { - case SerializationOptions.UseJson: - case SerializationOptions.Auto: + case SqlServerSerialization.UseJson: + case SqlServerSerialization.Auto: Assert.IsType(queryableFactory); break; - case SerializationOptions.UseXml: + case SqlServerSerialization.UseXml: Assert.IsType(queryableFactory); break; default: diff --git a/tests/QueryableValues.SqlServer.Tests/Integration/SimpleTypeTests.cs b/tests/QueryableValues.SqlServer.Tests/Integration/SimpleTypeTests.cs index dcd8571..13064d5 100644 --- a/tests/QueryableValues.SqlServer.Tests/Integration/SimpleTypeTests.cs +++ b/tests/QueryableValues.SqlServer.Tests/Integration/SimpleTypeTests.cs @@ -208,7 +208,7 @@ public async Task MustMatchSequenceOfChar() { var values = new[] { 'A', 'a', 'ᴭ', ' ', '\n', '\0', '\u0001' }; - if (_db.Options.WithSerializationOptions == SerializationOptions.UseXml) + if (_db.Options.WithSerializationOption == SqlServerSerialization.UseXml) { { var expected = new[] { 'A', 'a', '?', ' ', '\n', '?', '?' }; @@ -222,7 +222,7 @@ public async Task MustMatchSequenceOfChar() Assert.Equal(expected, actual); } } - else if (_db.Options.WithSerializationOptions == SerializationOptions.UseJson) + else if (_db.Options.WithSerializationOption == SqlServerSerialization.UseJson) { { var expected = new[] { 'A', 'a', '?', ' ', '\n', '\0', '\u0001' }; @@ -252,7 +252,7 @@ public async Task MustMatchSequenceOfString() { var values = new[] { "\0 ", "\u0001", "Test 1", "Test <2>", "Test &3", "😀", "ᴭ", "", " ", "\n", " \n", " \n ", "\r", "\r ", " Test\t1 ", "\U00010330" }; - if (_db.Options.WithSerializationOptions == SerializationOptions.UseXml) + if (_db.Options.WithSerializationOption == SqlServerSerialization.UseXml) { { var expected = new string[values.Length]; @@ -279,7 +279,7 @@ public async Task MustMatchSequenceOfString() Assert.Equal(expected, actual); } } - else if (_db.Options.WithSerializationOptions == SerializationOptions.UseJson) + else if (_db.Options.WithSerializationOption == SqlServerSerialization.UseJson) { { var expected = new string[values.Length]; @@ -735,7 +735,7 @@ public class JsonSimpleTypeTests : SimpleTypeTests { public JsonSimpleTypeTests(DbContextFixture contextFixture) : base(contextFixture) { - _db.Options.Serialization(SerializationOptions.UseJson); + _db.Options.Serialization(SqlServerSerialization.UseJson); } } @@ -744,7 +744,7 @@ public class XmlSimpleTypeTests : SimpleTypeTests { public XmlSimpleTypeTests(DbContextFixture contextFixture) : base(contextFixture) { - _db.Options.Serialization(SerializationOptions.UseXml); + _db.Options.Serialization(SqlServerSerialization.UseXml); } } } From 7dbc3877e39460d9f2e0747231187719b4bea190 Mon Sep 17 00:00:00 2001 From: yv989c Date: Sun, 26 Mar 2023 13:59:50 -0400 Subject: [PATCH 26/29] docs: tldr and integration tests --- README.md | 4 ++++ docs/README.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/README.md b/README.md index c1ed033..8699563 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ [![GitHub Stars](https://badgen.net/github/stars/yv989c/BlazarTech.QueryableValues?icon=github)][Repository] [![Nuget Downloads](https://badgen.net/nuget/dt/BlazarTech.QueryableValues.SqlServer?icon=nuget)][NuGet Package] +> 🤔💭 TLDR; By using QueryableValues, you can incorporate in-memory collections into your EF queries with outstanding performance and flexibility. + This library allows you to efficiently compose an [IEnumerable<T>] in your [Entity Framework Core] queries when using the [SQL Server Database Provider]. This is accomplished by using the `AsQueryableValues` extension method available on the [DbContext] class. Everything is evaluated on the server with a single round trip, in a way that preserves the query's [execution plan], even when the values behind the [IEnumerable<T>] are changed on subsequent executions. The supported types for `T` are: @@ -19,6 +21,8 @@ The supported types for `T` are: For a detailed explanation of the problem solved by QueryableValues, please continue reading [here][readme-background]. +> 💡 QueryableValues boasts over 120 integration tests that are executed on every supported version of EF. These tests ensure reliability and compatibility, giving you added confidence. + > 💡 Still on Entity Framework 6 (non-core)? Then [QueryableValues `EF6 Edition`](https://github.com/yv989c/BlazarTech.QueryableValues.EF6) is what you need. ## When Should You Use It? diff --git a/docs/README.md b/docs/README.md index 43a0ff3..3ba69bd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,6 +4,8 @@ [![GitHub Stars](https://badgen.net/github/stars/yv989c/BlazarTech.QueryableValues?icon=github)][Repository] [![Nuget Downloads](https://badgen.net/nuget/dt/BlazarTech.QueryableValues.SqlServer?icon=nuget)][NuGet Package] +> 🤔💭 TLDR; By using QueryableValues, you can incorporate in-memory collections into your EF queries with outstanding performance and flexibility. + This library allows you to efficiently compose an [IEnumerable<T>] in your [Entity Framework Core] queries when using the [SQL Server Database Provider]. This is accomplished by using the `AsQueryableValues` extension method available on the [DbContext] class. Everything is evaluated on the server with a single round trip, in a way that preserves the query's [execution plan], even when the values behind the [IEnumerable<T>] are changed on subsequent executions. The supported types for `T` are: @@ -15,6 +17,8 @@ The supported types for `T` are: For a detailed explanation of the problem solved by QueryableValues, please continue reading [here][readme-background]. +> 💡 QueryableValues boasts over 120 integration tests that are executed on every supported version of EF. These tests ensure reliability and compatibility, giving you added confidence. + > 💡 Still on Entity Framework 6 (non-core)? Then [QueryableValues `EF6 Edition`](https://github.com/yv989c/BlazarTech.QueryableValues.EF6) is what you need. ## When Should You Use It? From 493dadb801c2a4a97a27725cfe13bff170ac5942 Mon Sep 17 00:00:00 2001 From: yv989c Date: Sun, 26 Mar 2023 14:54:47 -0400 Subject: [PATCH 27/29] docs: new benchmarks and touches --- README.md | 182 +++++++++++++++++------------- docs/images/benchmarks/v7.2.0.png | Bin 0 -> 213103 bytes 2 files changed, 102 insertions(+), 80 deletions(-) create mode 100644 docs/images/benchmarks/v7.2.0.png diff --git a/README.md b/README.md index 8699563..7953511 100644 --- a/README.md +++ b/README.md @@ -175,103 +175,120 @@ var myQuery = > :warning: There is a limit of up to 10 properties for any given simple type (e.g. cannot have more than 10 [Int32] properties). Exceeding that limit will cause an exception and may also suggest that you should rethink your strategy. # Benchmarks -The following [benchmarks] consist of simple EF Core queries that have a dependency on a random sequence of [Int32] and [Guid] values via the `Contains` LINQ method. It shows the performance differences between not using and using QueryableValues. In practice, the benefits of using QueryableValues will be more dramatic on complex EF Core queries and busy environments. +The following [benchmarks] consist of simple EF Core queries that have a dependency on a random sequence of [Int32], [Guid], and [String] values via the `Contains` LINQ method. It shows the performance differences between not using and using QueryableValues. In practice, the benefits of using QueryableValues are more dramatic on complex EF Core queries and busy environments. ### Benchmarked Libraries | Package | Version | | ------- |:-------:| -| Microsoft.EntityFrameworkCore.SqlServer | 6.0.1 | -| BlazarTech.QueryableValues.SqlServer | 6.3.0 | +| Microsoft.EntityFrameworkCore.SqlServer | 7.0.4 | +| BlazarTech.QueryableValues.SqlServer | 7.2.0 | -### BenchmarkDotNet Configuration and System Specs +### BenchmarkDotNet System Specs and Configuration ``` -BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19042.1466 (20H2/October2020Update) -Intel Core i7-8650U CPU 1.90GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores -.NET SDK=6.0.101 - [Host] : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT - Job-GMTUEM : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT +BenchmarkDotNet=v0.13.5, OS=Windows 11 (10.0.22621.1413/22H2/2022Update/SunValley2) +AMD Ryzen 9 6900HS Creator Edition, 1 CPU, 16 logical and 8 physical cores +.NET SDK=7.0.202 + [Host] : .NET 6.0.15 (6.0.1523.11507), X64 RyuJIT AVX2 + Job-OFVMJD : .NET 6.0.15 (6.0.1523.11507), X64 RyuJIT AVX2 Server=True InvocationCount=200 IterationCount=25 RunStrategy=Monitoring UnrollFactor=1 WarmupCount=1 ``` ### SQL Server Instance Specs ``` -Microsoft SQL Server 2017 (RTM-GDR) (KB4583456) - 14.0.2037.2 (X64) -Nov 2 2020 19:19:59 -Copyright (C) 2017 Microsoft Corporation -Express Edition (64-bit) on Windows 10 Pro 10.0 (Build 19042: ) (Hypervisor) +Microsoft SQL Server 2022 (RTM) - 16.0.1000.6 (X64) +Oct 8 2022 05:58:25 +Copyright (C) 2022 Microsoft Corporation +Express Edition (64-bit) on Windows 10 Pro 10.0 (Build 22621: ) (Hypervisor) ``` -- The SQL Server instance was running in the same system where the benchmark was executed. +- The SQL Server instance was running in the same system where the benchmarks were executed. - Shared Memory is the only network protocol that's enabled on this instance. +### Query Duration - Without vs. With (XML) vs. With (JSON) -### Results for Int32 +**Legend:** -![Benchmarks Int32 Values][BenchmarksInt32] +- **Without:** Plain EF. +- **With (XML):** EF with QueryableValues using the XML serializer. +- **With (JSON):** EF with QueryableValues using the JSON serializer. -
- -| Method | Values | Mean (us) | Error (us) | Std Dev (us) | Median (us) | Ratio | RatioSD | Allocated | -|---------|--------|--------------|--------------|----------------|---------------|-------|---------|-----------| -| Without | 2 | 921.20 | 31.30 | 41.78 | 903.80 | 1.00 | 0.00 | 20 KB | -| With | 2 | 734.30 | 45.28 | 60.44 | 696.10 | 0.80 | 0.04 | 51 KB | -| Without | 4 | 997.80 | 31.79 | 42.44 | 981.10 | 1.00 | 0.00 | 21 KB | -| With | 4 | 779.40 | 47.22 | 63.04 | 738.70 | 0.78 | 0.05 | 51 KB | -| Without | 8 | 1,081.00 | 31.26 | 41.74 | 1,061.30 | 1.00 | 0.00 | 21 KB | -| With | 8 | 814.20 | 47.34 | 63.20 | 775.70 | 0.75 | 0.04 | 51 KB | -| Without | 16 | 1,331.70 | 88.81 | 118.56 | 1,283.40 | 1.00 | 0.00 | 23 KB | -| With | 16 | 872.70 | 42.46 | 56.68 | 840.30 | 0.66 | 0.06 | 52 KB | -| Without | 32 | 1,731.40 | 40.59 | 54.18 | 1,732.60 | 1.00 | 0.00 | 26 KB | -| With | 32 | 1,006.00 | 47.61 | 63.56 | 973.60 | 0.58 | 0.03 | 53 KB | -| Without | 64 | 2,615.40 | 103.77 | 138.53 | 2,540.20 | 1.00 | 0.00 | 31 KB | -| With | 64 | 1,264.20 | 36.95 | 49.33 | 1,239.90 | 0.48 | 0.03 | 55 KB | -| Without | 128 | 5,687.30 | 200.05 | 267.06 | 5,588.20 | 1.00 | 0.00 | 41 KB | -| With | 128 | 1,917.00 | 34.06 | 45.47 | 1,897.90 | 0.34 | 0.02 | 60 KB | -| Without | 256 | 10,565.00 | 186.05 | 248.37 | 10,473.00 | 1.00 | 0.00 | 63 KB | -| With | 256 | 2,977.00 | 29.38 | 39.23 | 2,964.50 | 0.28 | 0.01 | 69 KB | -| Without | 512 | 20,110.50 | 452.28 | 603.79 | 20,108.30 | 1.00 | 0.00 | 106 KB | -| With | 512 | 5,313.10 | 47.66 | 63.62 | 5,340.80 | 0.26 | 0.01 | 88 KB | -| Without | 1024 | 46,599.30 | 4,286.13 | 5,721.87 | 48,194.20 | 1.00 | 0.00 | 192 KB | -| With | 1024 | 11,614.40 | 85.81 | 114.55 | 11,619.80 | 0.25 | 0.03 | 128 KB | -| Without | 2048 | 105,096.90 | 5,359.60 | 7,154.92 | 106,405.10 | 1.00 | 0.00 | 363 KB | -| With | 2048 | 19,481.40 | 66.66 | 88.99 | 19,474.80 | 0.19 | 0.01 | 213 KB | -| Without | 4096 | 177,245.80 | 1,812.40 | 2,419.51 | 176,767.90 | 1.00 | 0.00 | 706 KB | -| With | 4096 | 38,743.00 | 2,422.07 | 3,233.40 | 37,414.70 | 0.22 | 0.02 | 368 KB | - -
- -### Results for Guid - -![Benchmarks Guid Values][BenchmarksGuid] +![BenchmarksChart][BenchmarksChart]
-| Method | Values | Mean (us) | Error (us) | Std Dev (us) | Median (us) | Ratio | RatioSD | Allocated | -|---------|--------|--------------|--------------|----------------|---------------|-------|---------|-----------| -| Without | 2 | 895.60 | 30.64 | 40.91 | 877.90 | 1.00 | 0.00 | 21 KB | -| With | 2 | 741.80 | 46.44 | 62.00 | 704.40 | 0.83 | 0.04 | 51 KB | -| Without | 4 | 968.90 | 33.69 | 44.97 | 950.40 | 1.00 | 0.00 | 22 KB | -| With | 4 | 727.00 | 43.20 | 57.68 | 689.80 | 0.75 | 0.04 | 52 KB | -| Without | 8 | 1,075.50 | 34.88 | 46.57 | 1,054.90 | 1.00 | 0.00 | 23 KB | -| With | 8 | 773.10 | 42.45 | 56.67 | 737.10 | 0.72 | 0.04 | 53 KB | -| Without | 16 | 1,372.60 | 66.21 | 88.39 | 1,383.80 | 1.00 | 0.00 | 26 KB | -| With | 16 | 808.90 | 40.12 | 53.55 | 777.80 | 0.59 | 0.06 | 55 KB | -| Without | 32 | 1,710.70 | 26.25 | 35.04 | 1,699.90 | 1.00 | 0.00 | 33 KB | -| With | 32 | 869.80 | 49.27 | 65.78 | 830.40 | 0.51 | 0.03 | 59 KB | -| Without | 64 | 2,656.60 | 30.28 | 40.43 | 2,652.30 | 1.00 | 0.00 | 47 KB | -| With | 64 | 1,038.70 | 58.99 | 78.75 | 994.40 | 0.39 | 0.03 | 67 KB | -| Without | 128 | 5,415.90 | 45.76 | 61.09 | 5,417.00 | 1.00 | 0.00 | 74 KB | -| With | 128 | 1,456.30 | 53.76 | 71.77 | 1,424.10 | 0.27 | 0.02 | 84 KB | -| Without | 256 | 9,461.50 | 45.09 | 60.20 | 9,469.10 | 1.00 | 0.00 | 128 KB | -| With | 256 | 2,156.00 | 36.01 | 48.07 | 2,139.30 | 0.23 | 0.00 | 120 KB | -| Without | 512 | 18,015.10 | 117.47 | 156.82 | 17,946.50 | 1.00 | 0.00 | 219 KB | -| With | 512 | 3,511.30 | 62.41 | 83.32 | 3,460.80 | 0.19 | 0.00 | 197 KB | -| Without | 1024 | 44,525.60 | 754.94 | 1,007.82 | 44,601.80 | 1.00 | 0.00 | 419 KB | -| With | 1024 | 7,825.80 | 72.45 | 96.72 | 7,808.20 | 0.18 | 0.00 | 319 KB | -| Without | 2048 | 83,843.30 | 778.80 | 1,039.68 | 83,954.70 | 1.00 | 0.00 | 801 KB | -| With | 2048 | 12,372.40 | 207.91 | 277.55 | 12,232.20 | 0.15 | 0.00 | 596 KB | -| Without | 4096 | 217,255.80 | 3,458.95 | 4,617.60 | 216,353.20 | 1.00 | 0.00 | 1,566 KB | -| With | 4096 | 24,981.10 | 274.10 | 365.92 | 25,116.70 | 0.12 | 0.00 | 1,132 KB | +| Method | Type | NumberOfValues | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Gen1 | Gen2 | Allocated | Alloc Ratio | +|--------- |------- |--------------- |-------------:|----------:|----------:|-------------:|------:|--------:|-------:|-------:|-------:|-----------:|------------:| +| Without | Int32 | 2 | 824.3 us | 26.03 us | 34.75 us | 808.9 us | 1.00 | 0.00 | - | - | - | 20.26 KB | 1.00 | +| WithXml | Int32 | 2 | 508.7 us | 32.46 us | 43.34 us | 504.3 us | 0.62 | 0.04 | - | - | - | 41.37 KB | 2.04 | +| WithJson | Int32 | 2 | 431.7 us | 35.52 us | 47.41 us | 446.8 us | 0.52 | 0.05 | - | - | - | 41.5 KB | 2.05 | +| | | | | | | | | | | | | | | +| Without | Int32 | 8 | 964.8 us | 25.05 us | 33.44 us | 954.6 us | 1.00 | 0.00 | - | - | - | 21.17 KB | 1.00 | +| WithXml | Int32 | 8 | 548.2 us | 34.29 us | 45.78 us | 537.0 us | 0.57 | 0.04 | - | - | - | 41.33 KB | 1.95 | +| WithJson | Int32 | 8 | 445.1 us | 34.28 us | 45.76 us | 453.6 us | 0.46 | 0.04 | - | - | - | 41.56 KB | 1.96 | +| | | | | | | | | | | | | | | +| Without | Int32 | 32 | 1,519.3 us | 34.23 us | 45.69 us | 1,494.4 us | 1.00 | 0.00 | - | - | - | 25.45 KB | 1.00 | +| WithXml | Int32 | 32 | 687.5 us | 32.29 us | 43.10 us | 664.9 us | 0.45 | 0.03 | - | - | - | 41.52 KB | 1.63 | +| WithJson | Int32 | 32 | 448.1 us | 38.22 us | 51.03 us | 425.9 us | 0.30 | 0.04 | - | - | - | 41.61 KB | 1.63 | +| | | | | | | | | | | | | | | +| Without | Int32 | 128 | 5,470.2 us | 25.34 us | 33.83 us | 5,473.2 us | 1.00 | 0.00 | - | - | - | 41.18 KB | 1.00 | +| WithXml | Int32 | 128 | 1,334.4 us | 37.80 us | 50.47 us | 1,316.5 us | 0.24 | 0.01 | - | - | - | 44.02 KB | 1.07 | +| WithJson | Int32 | 128 | 498.9 us | 33.69 us | 44.97 us | 498.1 us | 0.09 | 0.01 | - | - | - | 42.53 KB | 1.03 | +| | | | | | | | | | | | | | | +| Without | Int32 | 512 | 17,572.2 us | 68.50 us | 91.45 us | 17,566.4 us | 1.00 | 0.00 | - | - | - | 105.67 KB | 1.00 | +| WithXml | Int32 | 512 | 4,016.2 us | 30.74 us | 41.04 us | 4,014.4 us | 0.23 | 0.00 | - | - | - | 52.18 KB | 0.49 | +| WithJson | Int32 | 512 | 685.0 us | 30.40 us | 40.59 us | 661.9 us | 0.04 | 0.00 | - | - | - | 46.37 KB | 0.44 | +| | | | | | | | | | | | | | | +| Without | Int32 | 2048 | 71,616.8 us | 677.00 us | 903.77 us | 71,227.6 us | 1.00 | 0.00 | - | - | - | 363.17 KB | 1.00 | +| WithXml | Int32 | 2048 | 14,045.8 us | 50.55 us | 67.48 us | 14,029.9 us | 0.20 | 0.00 | - | - | - | 84.85 KB | 0.23 | +| WithJson | Int32 | 2048 | 1,577.1 us | 32.17 us | 42.95 us | 1,564.8 us | 0.02 | 0.00 | - | - | - | 61.07 KB | 0.17 | +| | | | | | | | | | | | | | | +| Without | Guid | 2 | 788.9 us | 20.31 us | 27.11 us | 778.1 us | 1.00 | 0.00 | - | - | - | 20.74 KB | 1.00 | +| WithXml | Guid | 2 | 487.6 us | 30.51 us | 40.74 us | 487.7 us | 0.62 | 0.04 | - | - | - | 41.23 KB | 1.99 | +| WithJson | Guid | 2 | 434.7 us | 33.42 us | 44.61 us | 443.3 us | 0.55 | 0.04 | - | - | - | 41.19 KB | 1.99 | +| | | | | | | | | | | | | | | +| Without | Guid | 8 | 939.1 us | 29.24 us | 39.04 us | 921.1 us | 1.00 | 0.00 | - | - | - | 23.49 KB | 1.00 | +| WithXml | Guid | 8 | 515.1 us | 32.95 us | 43.99 us | 509.2 us | 0.55 | 0.04 | - | - | - | 42.23 KB | 1.80 | +| WithJson | Guid | 8 | 450.0 us | 33.55 us | 44.79 us | 461.4 us | 0.48 | 0.04 | - | - | - | 41.98 KB | 1.79 | +| | | | | | | | | | | | | | | +| Without | Guid | 32 | 1,566.2 us | 43.12 us | 57.56 us | 1,551.3 us | 1.00 | 0.00 | - | - | - | 33.24 KB | 1.00 | +| WithXml | Guid | 32 | 607.3 us | 33.01 us | 44.07 us | 587.0 us | 0.39 | 0.03 | - | - | - | 43.58 KB | 1.31 | +| WithJson | Guid | 32 | 488.4 us | 32.86 us | 43.87 us | 487.3 us | 0.31 | 0.03 | - | - | - | 43.48 KB | 1.31 | +| | | | | | | | | | | | | | | +| Without | Guid | 128 | 5,140.0 us | 52.22 us | 69.71 us | 5,138.2 us | 1.00 | 0.00 | - | - | - | 74.11 KB | 1.00 | +| WithXml | Guid | 128 | 987.8 us | 37.30 us | 49.79 us | 965.0 us | 0.19 | 0.01 | - | - | - | 51.97 KB | 0.70 | +| WithJson | Guid | 128 | 665.9 us | 38.37 us | 51.23 us | 636.8 us | 0.13 | 0.01 | - | - | - | 51.12 KB | 0.69 | +| | | | | | | | | | | | | | | +| Without | Guid | 512 | 16,031.0 us | 74.08 us | 98.89 us | 16,023.7 us | 1.00 | 0.00 | - | - | - | 219.5 KB | 1.00 | +| WithXml | Guid | 512 | 2,528.8 us | 38.80 us | 51.79 us | 2,517.7 us | 0.16 | 0.00 | - | - | - | 84.36 KB | 0.38 | +| WithJson | Guid | 512 | 1,368.8 us | 22.42 us | 29.93 us | 1,355.1 us | 0.09 | 0.00 | - | - | - | 80.08 KB | 0.36 | +| | | | | | | | | | | | | | | +| Without | Guid | 2048 | 71,956.6 us | 688.35 us | 918.93 us | 72,148.6 us | 1.00 | 0.00 | - | - | - | 801.13 KB | 1.00 | +| WithXml | Guid | 2048 | 9,399.9 us | 76.33 us | 101.90 us | 9,359.8 us | 0.13 | 0.00 | 5.0000 | 5.0000 | 5.0000 | 213.42 KB | 0.27 | +| WithJson | Guid | 2048 | 4,463.6 us | 36.90 us | 49.26 us | 4,442.6 us | 0.06 | 0.00 | - | - | - | 197.4 KB | 0.25 | +| | | | | | | | | | | | | | | +| Without | String | 2 | 858.7 us | 23.34 us | 31.16 us | 846.2 us | 1.00 | 0.00 | - | - | - | 21.44 KB | 1.00 | +| WithXml | String | 2 | 637.4 us | 35.57 us | 47.48 us | 626.0 us | 0.74 | 0.04 | - | - | - | 55.52 KB | 2.59 | +| WithJson | String | 2 | 534.5 us | 30.81 us | 41.13 us | 528.7 us | 0.62 | 0.03 | - | - | - | 42.83 KB | 2.00 | +| | | | | | | | | | | | | | | +| Without | String | 8 | 1,028.9 us | 24.07 us | 32.13 us | 1,015.2 us | 1.00 | 0.00 | - | - | - | 25.55 KB | 1.00 | +| WithXml | String | 8 | 737.8 us | 44.23 us | 59.05 us | 727.5 us | 0.72 | 0.04 | - | - | - | 56.98 KB | 2.23 | +| WithJson | String | 8 | 641.8 us | 34.63 us | 46.23 us | 640.1 us | 0.62 | 0.04 | - | - | - | 43.64 KB | 1.71 | +| | | | | | | | | | | | | | | +| Without | String | 32 | 1,692.5 us | 23.43 us | 31.27 us | 1,684.7 us | 1.00 | 0.00 | - | - | - | 41.84 KB | 1.00 | +| WithXml | String | 32 | 1,016.7 us | 56.75 us | 75.76 us | 976.6 us | 0.60 | 0.04 | - | - | - | 60.35 KB | 1.44 | +| WithJson | String | 32 | 871.5 us | 39.02 us | 52.10 us | 843.8 us | 0.51 | 0.03 | - | - | - | 47.29 KB | 1.13 | +| | | | | | | | | | | | | | | +| Without | String | 128 | 7,665.5 us | 28.53 us | 38.09 us | 7,662.0 us | 1.00 | 0.00 | - | - | - | 103.65 KB | 1.00 | +| WithXml | String | 128 | 2,392.2 us | 35.64 us | 47.57 us | 2,379.7 us | 0.31 | 0.01 | - | - | - | 74.85 KB | 0.72 | +| WithJson | String | 128 | 2,063.6 us | 26.61 us | 35.53 us | 2,063.5 us | 0.27 | 0.01 | - | - | - | 61.2 KB | 0.59 | +| | | | | | | | | | | | | | | +| Without | String | 512 | 26,444.7 us | 102.44 us | 136.75 us | 26,421.0 us | 1.00 | 0.00 | - | - | - | 343.51 KB | 1.00 | +| WithXml | String | 512 | 8,134.2 us | 32.51 us | 43.41 us | 8,125.8 us | 0.31 | 0.00 | - | - | - | 132.34 KB | 0.39 | +| WithJson | String | 512 | 7,210.9 us | 33.10 us | 44.18 us | 7,199.6 us | 0.27 | 0.00 | - | - | - | 116.42 KB | 0.34 | +| | | | | | | | | | | | | | | +| Without | String | 2048 | 112,512.8 us | 443.78 us | 592.43 us | 112,461.1 us | 1.00 | 0.00 | 5.0000 | - | - | 1310.32 KB | 1.00 | +| WithXml | String | 2048 | 32,080.3 us | 138.18 us | 184.47 us | 32,075.1 us | 0.29 | 0.00 | - | - | - | 361.05 KB | 0.28 | +| WithJson | String | 2048 | 28,929.1 us | 84.67 us | 113.03 us | 28,917.8 us | 0.26 | 0.00 | - | - | - | 336.47 KB | 0.26 |
@@ -385,14 +402,17 @@ As expected, none of the queries in the orange section hit the cache. On the oth Now, focus your attention to the first query of the green section. Here you can observe that there's a cost associated with this technique, but this cost can be offset in the long run, especially when your queries are not trivial like the ones in these examples. ## What Makes This Work? 🤓 + +> 🎉 QueryableValues now supports JSON serialization, which improves its performance compared to using XML. By default, QueryableValues will attempt to use JSON if it is supported. + QueryableValues makes use of the XML parsing capabilities in SQL Server, which are available in all the supported versions of SQL Server to date. The provided sequence of values are serialized as XML and embedded in the underlying SQL query using a native XML parameter, then it uses SQL Server's XML type methods to project the query in a way that can be mapped by [Entity Framework Core]. This is a technique that I have not seen being used by other popular libraries that aim to solve this problem. It is superior from a latency standpoint because it resolves the query with a single round trip to the database and most importantly, it preserves the query's [execution plan] even when the content of the XML is changed. ## One More Thing 👀 -The `AsQueryableValues` extension method allows you to treat a sequence of values as you normally would if these were another entity in your [DbContext]. The type returned by the extension is an [IQueryable<T>] that can be composed with other entities in your query. +The `AsQueryableValues` extension method allows you to treat a sequence of values as you normally would if they were another entity in your [DbContext]. The type returned by the extension is an [IQueryable<T>] that can be composed with other entities in your query. -For example, you can do one or more joins like this and it is totally fine: +For example, you can perform one or more joins like this and it is completely fine: ```c# var myQuery = from i in dbContext.MyEntities @@ -404,6 +424,9 @@ var myQuery = i.PropA }; ``` + +Isn't that great? 🥰 + ## Did You Find a 🐛 or Have an 💡? PRs are welcome! 🙂 @@ -439,5 +462,4 @@ PRs are welcome! 🙂 [Repository]: https://github.com/yv989c/BlazarTech.QueryableValues [benchmarks]: /benchmarks/QueryableValues.SqlServer.Benchmarks -[BenchmarksInt32]: /docs/images/benchmarks/int32-v6.3.0.png -[BenchmarksGuid]: /docs/images/benchmarks/guid-v6.3.0.png +[BenchmarksChart]: /docs/images/benchmarks/v7.2.0.png \ No newline at end of file diff --git a/docs/images/benchmarks/v7.2.0.png b/docs/images/benchmarks/v7.2.0.png new file mode 100644 index 0000000000000000000000000000000000000000..1b888403b85f46223e2600aba5fba7a0090a9cf7 GIT binary patch literal 213103 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 Bh?)QZ literal 0 HcmV?d00001 From bf9c36469823c5a8db1f5c89afa3c71b90ee7a86 Mon Sep 17 00:00:00 2001 From: yv989c Date: Sun, 26 Mar 2023 21:41:23 -0400 Subject: [PATCH 28/29] docs: adds interactive chart and minor tweaks --- README.md | 8 +++++--- docs/README.md | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7953511..4e752d9 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ The supported types for `T` are: For a detailed explanation of the problem solved by QueryableValues, please continue reading [here][readme-background]. -> 💡 QueryableValues boasts over 120 integration tests that are executed on every supported version of EF. These tests ensure reliability and compatibility, giving you added confidence. +> ✅ QueryableValues boasts over 120 integration tests that are executed on every supported version of EF. These tests ensure reliability and compatibility, giving you added confidence. > 💡 Still on Entity Framework 6 (non-core)? Then [QueryableValues `EF6 Edition`](https://github.com/yv989c/BlazarTech.QueryableValues.EF6) is what you need. @@ -83,6 +83,7 @@ public class Startup } } ``` +> 💡 Pro-tip: `UseQueryableValues` offers an optional `options` delegate for additional configurations. ## How Do You Use It? The `AsQueryableValues` extension method is provided by the `BlazarTech.QueryableValues` namespace; therefore, you must add the following `using` directive to your source code file for it to appear as a method of your [DbContext] instance: @@ -212,7 +213,7 @@ Express Edition (64-bit) on Windows 10 Pro 10.0 (Build 22621: ) (Hyperviso - **With (XML):** EF with QueryableValues using the XML serializer. - **With (JSON):** EF with QueryableValues using the JSON serializer. -![BenchmarksChart][BenchmarksChart] +[![Benchmarks Chart][BenchmarksChart]][BenchmarksChartInteractive]

@@ -462,4 +463,5 @@ PRs are welcome! 🙂 [Repository]: https://github.com/yv989c/BlazarTech.QueryableValues [benchmarks]: /benchmarks/QueryableValues.SqlServer.Benchmarks -[BenchmarksChart]: /docs/images/benchmarks/v7.2.0.png \ No newline at end of file +[BenchmarksChart]: /docs/images/benchmarks/v7.2.0.png +[BenchmarksChartInteractive]: https://chartbenchmark.net/?src=repo#shared=%7B%22results%22%3A%22BenchmarkDotNet%3Dv0.13.5%2C%20OS%3DWindows%2011%20(10.0.22621.1413%2F22H2%2F2022Update%2FSunValley2)%5CnAMD%20Ryzen%209%206900HS%20Creator%20Edition%2C%201%20CPU%2C%2016%20logical%20and%208%20physical%20cores%5Cn.NET%20SDK%3D7.0.202%5Cn%20%20%5BHost%5D%20%20%20%20%20%3A%20.NET%206.0.15%20(6.0.1523.11507)%2C%20X64%20RyuJIT%20AVX2%5Cn%20%20Job-OFVMJD%20%3A%20.NET%206.0.15%20(6.0.1523.11507)%2C%20X64%20RyuJIT%20AVX2%5Cn%5CnServer%3DTrue%20%20InvocationCount%3D200%20%20IterationCount%3D25%5CnRunStrategy%3DMonitoring%20%20UnrollFactor%3D1%20%20WarmupCount%3D1%5Cn%5Cn%7C%20%20%20Method%20%7C%20%20%20Type%20%7C%20NumberOfValues%20%7C%20%20%20%20%20%20%20%20%20Mean%20%7C%20%20%20%20%20Error%20%7C%20%20%20%20StdDev%20%7C%20%20%20%20%20%20%20Median%20%7C%20Ratio%20%7C%20RatioSD%20%7C%20%20%20Gen0%20%7C%20%20%20Gen1%20%7C%20%20%20Gen2%20%7C%20%20Allocated%20%7C%20Alloc%20Ratio%20%7C%5Cn%7C---------%20%7C-------%20%7C---------------%20%7C-------------%3A%7C----------%3A%7C----------%3A%7C-------------%3A%7C------%3A%7C--------%3A%7C-------%3A%7C-------%3A%7C-------%3A%7C-----------%3A%7C------------%3A%7C%5Cn%7C%20%20Without%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%202%20%7C%20%20%20%20%20824.3%20us%20%7C%20%2026.03%20us%20%7C%20%2034.75%20us%20%7C%20%20%20%20%20808.9%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2020.26%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%202%20%7C%20%20%20%20%20508.7%20us%20%7C%20%2032.46%20us%20%7C%20%2043.34%20us%20%7C%20%20%20%20%20504.3%20us%20%7C%20%200.62%20%7C%20%20%20%200.04%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2041.37%20KB%20%7C%20%20%20%20%20%20%20%202.04%20%7C%5Cn%7C%20WithJson%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%202%20%7C%20%20%20%20%20431.7%20us%20%7C%20%2035.52%20us%20%7C%20%2047.41%20us%20%7C%20%20%20%20%20446.8%20us%20%7C%20%200.52%20%7C%20%20%20%200.05%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%2041.5%20KB%20%7C%20%20%20%20%20%20%20%202.05%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%208%20%7C%20%20%20%20%20964.8%20us%20%7C%20%2025.05%20us%20%7C%20%2033.44%20us%20%7C%20%20%20%20%20954.6%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2021.17%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%208%20%7C%20%20%20%20%20548.2%20us%20%7C%20%2034.29%20us%20%7C%20%2045.78%20us%20%7C%20%20%20%20%20537.0%20us%20%7C%20%200.57%20%7C%20%20%20%200.04%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2041.33%20KB%20%7C%20%20%20%20%20%20%20%201.95%20%7C%5Cn%7C%20WithJson%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%208%20%7C%20%20%20%20%20445.1%20us%20%7C%20%2034.28%20us%20%7C%20%2045.76%20us%20%7C%20%20%20%20%20453.6%20us%20%7C%20%200.46%20%7C%20%20%20%200.04%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2041.56%20KB%20%7C%20%20%20%20%20%20%20%201.96%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%2032%20%7C%20%20%201%2C519.3%20us%20%7C%20%2034.23%20us%20%7C%20%2045.69%20us%20%7C%20%20%201%2C494.4%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2025.45%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%2032%20%7C%20%20%20%20%20687.5%20us%20%7C%20%2032.29%20us%20%7C%20%2043.10%20us%20%7C%20%20%20%20%20664.9%20us%20%7C%20%200.45%20%7C%20%20%20%200.03%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2041.52%20KB%20%7C%20%20%20%20%20%20%20%201.63%20%7C%5Cn%7C%20WithJson%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%2032%20%7C%20%20%20%20%20448.1%20us%20%7C%20%2038.22%20us%20%7C%20%2051.03%20us%20%7C%20%20%20%20%20425.9%20us%20%7C%20%200.30%20%7C%20%20%20%200.04%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2041.61%20KB%20%7C%20%20%20%20%20%20%20%201.63%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20128%20%7C%20%20%205%2C470.2%20us%20%7C%20%2025.34%20us%20%7C%20%2033.83%20us%20%7C%20%20%205%2C473.2%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2041.18%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20128%20%7C%20%20%201%2C334.4%20us%20%7C%20%2037.80%20us%20%7C%20%2050.47%20us%20%7C%20%20%201%2C316.5%20us%20%7C%20%200.24%20%7C%20%20%20%200.01%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2044.02%20KB%20%7C%20%20%20%20%20%20%20%201.07%20%7C%5Cn%7C%20WithJson%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20128%20%7C%20%20%20%20%20498.9%20us%20%7C%20%2033.69%20us%20%7C%20%2044.97%20us%20%7C%20%20%20%20%20498.1%20us%20%7C%20%200.09%20%7C%20%20%20%200.01%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2042.53%20KB%20%7C%20%20%20%20%20%20%20%201.03%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20512%20%7C%20%2017%2C572.2%20us%20%7C%20%2068.50%20us%20%7C%20%2091.45%20us%20%7C%20%2017%2C566.4%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20105.67%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20512%20%7C%20%20%204%2C016.2%20us%20%7C%20%2030.74%20us%20%7C%20%2041.04%20us%20%7C%20%20%204%2C014.4%20us%20%7C%20%200.23%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2052.18%20KB%20%7C%20%20%20%20%20%20%20%200.49%20%7C%5Cn%7C%20WithJson%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%20%20512%20%7C%20%20%20%20%20685.0%20us%20%7C%20%2030.40%20us%20%7C%20%2040.59%20us%20%7C%20%20%20%20%20661.9%20us%20%7C%20%200.04%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2046.37%20KB%20%7C%20%20%20%20%20%20%20%200.44%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%202048%20%7C%20%2071%2C616.8%20us%20%7C%20677.00%20us%20%7C%20903.77%20us%20%7C%20%2071%2C227.6%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20363.17%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%202048%20%7C%20%2014%2C045.8%20us%20%7C%20%2050.55%20us%20%7C%20%2067.48%20us%20%7C%20%2014%2C029.9%20us%20%7C%20%200.20%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2084.85%20KB%20%7C%20%20%20%20%20%20%20%200.23%20%7C%5Cn%7C%20WithJson%20%7C%20%20Int32%20%7C%20%20%20%20%20%20%20%20%20%20%202048%20%7C%20%20%201%2C577.1%20us%20%7C%20%2032.17%20us%20%7C%20%2042.95%20us%20%7C%20%20%201%2C564.8%20us%20%7C%20%200.02%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2061.07%20KB%20%7C%20%20%20%20%20%20%20%200.17%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%202%20%7C%20%20%20%20%20788.9%20us%20%7C%20%2020.31%20us%20%7C%20%2027.11%20us%20%7C%20%20%20%20%20778.1%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2020.74%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%202%20%7C%20%20%20%20%20487.6%20us%20%7C%20%2030.51%20us%20%7C%20%2040.74%20us%20%7C%20%20%20%20%20487.7%20us%20%7C%20%200.62%20%7C%20%20%20%200.04%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2041.23%20KB%20%7C%20%20%20%20%20%20%20%201.99%20%7C%5Cn%7C%20WithJson%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%202%20%7C%20%20%20%20%20434.7%20us%20%7C%20%2033.42%20us%20%7C%20%2044.61%20us%20%7C%20%20%20%20%20443.3%20us%20%7C%20%200.55%20%7C%20%20%20%200.04%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2041.19%20KB%20%7C%20%20%20%20%20%20%20%201.99%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%208%20%7C%20%20%20%20%20939.1%20us%20%7C%20%2029.24%20us%20%7C%20%2039.04%20us%20%7C%20%20%20%20%20921.1%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2023.49%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%208%20%7C%20%20%20%20%20515.1%20us%20%7C%20%2032.95%20us%20%7C%20%2043.99%20us%20%7C%20%20%20%20%20509.2%20us%20%7C%20%200.55%20%7C%20%20%20%200.04%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2042.23%20KB%20%7C%20%20%20%20%20%20%20%201.80%20%7C%5Cn%7C%20WithJson%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%208%20%7C%20%20%20%20%20450.0%20us%20%7C%20%2033.55%20us%20%7C%20%2044.79%20us%20%7C%20%20%20%20%20461.4%20us%20%7C%20%200.48%20%7C%20%20%20%200.04%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2041.98%20KB%20%7C%20%20%20%20%20%20%20%201.79%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%2032%20%7C%20%20%201%2C566.2%20us%20%7C%20%2043.12%20us%20%7C%20%2057.56%20us%20%7C%20%20%201%2C551.3%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2033.24%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%2032%20%7C%20%20%20%20%20607.3%20us%20%7C%20%2033.01%20us%20%7C%20%2044.07%20us%20%7C%20%20%20%20%20587.0%20us%20%7C%20%200.39%20%7C%20%20%20%200.03%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2043.58%20KB%20%7C%20%20%20%20%20%20%20%201.31%20%7C%5Cn%7C%20WithJson%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%2032%20%7C%20%20%20%20%20488.4%20us%20%7C%20%2032.86%20us%20%7C%20%2043.87%20us%20%7C%20%20%20%20%20487.3%20us%20%7C%20%200.31%20%7C%20%20%20%200.03%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2043.48%20KB%20%7C%20%20%20%20%20%20%20%201.31%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20128%20%7C%20%20%205%2C140.0%20us%20%7C%20%2052.22%20us%20%7C%20%2069.71%20us%20%7C%20%20%205%2C138.2%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2074.11%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20128%20%7C%20%20%20%20%20987.8%20us%20%7C%20%2037.30%20us%20%7C%20%2049.79%20us%20%7C%20%20%20%20%20965.0%20us%20%7C%20%200.19%20%7C%20%20%20%200.01%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2051.97%20KB%20%7C%20%20%20%20%20%20%20%200.70%20%7C%5Cn%7C%20WithJson%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20128%20%7C%20%20%20%20%20665.9%20us%20%7C%20%2038.37%20us%20%7C%20%2051.23%20us%20%7C%20%20%20%20%20636.8%20us%20%7C%20%200.13%20%7C%20%20%20%200.01%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2051.12%20KB%20%7C%20%20%20%20%20%20%20%200.69%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20512%20%7C%20%2016%2C031.0%20us%20%7C%20%2074.08%20us%20%7C%20%2098.89%20us%20%7C%20%2016%2C023.7%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20219.5%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20512%20%7C%20%20%202%2C528.8%20us%20%7C%20%2038.80%20us%20%7C%20%2051.79%20us%20%7C%20%20%202%2C517.7%20us%20%7C%20%200.16%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2084.36%20KB%20%7C%20%20%20%20%20%20%20%200.38%20%7C%5Cn%7C%20WithJson%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%20%20512%20%7C%20%20%201%2C368.8%20us%20%7C%20%2022.42%20us%20%7C%20%2029.93%20us%20%7C%20%20%201%2C355.1%20us%20%7C%20%200.09%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2080.08%20KB%20%7C%20%20%20%20%20%20%20%200.36%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%202048%20%7C%20%2071%2C956.6%20us%20%7C%20688.35%20us%20%7C%20918.93%20us%20%7C%20%2072%2C148.6%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20801.13%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%202048%20%7C%20%20%209%2C399.9%20us%20%7C%20%2076.33%20us%20%7C%20101.90%20us%20%7C%20%20%209%2C359.8%20us%20%7C%20%200.13%20%7C%20%20%20%200.00%20%7C%205.0000%20%7C%205.0000%20%7C%205.0000%20%7C%20%20213.42%20KB%20%7C%20%20%20%20%20%20%20%200.27%20%7C%5Cn%7C%20WithJson%20%7C%20%20%20Guid%20%7C%20%20%20%20%20%20%20%20%20%20%202048%20%7C%20%20%204%2C463.6%20us%20%7C%20%2036.90%20us%20%7C%20%2049.26%20us%20%7C%20%20%204%2C442.6%20us%20%7C%20%200.06%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20197.4%20KB%20%7C%20%20%20%20%20%20%20%200.25%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%202%20%7C%20%20%20%20%20858.7%20us%20%7C%20%2023.34%20us%20%7C%20%2031.16%20us%20%7C%20%20%20%20%20846.2%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2021.44%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%202%20%7C%20%20%20%20%20637.4%20us%20%7C%20%2035.57%20us%20%7C%20%2047.48%20us%20%7C%20%20%20%20%20626.0%20us%20%7C%20%200.74%20%7C%20%20%20%200.04%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2055.52%20KB%20%7C%20%20%20%20%20%20%20%202.59%20%7C%5Cn%7C%20WithJson%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%202%20%7C%20%20%20%20%20534.5%20us%20%7C%20%2030.81%20us%20%7C%20%2041.13%20us%20%7C%20%20%20%20%20528.7%20us%20%7C%20%200.62%20%7C%20%20%20%200.03%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2042.83%20KB%20%7C%20%20%20%20%20%20%20%202.00%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%208%20%7C%20%20%201%2C028.9%20us%20%7C%20%2024.07%20us%20%7C%20%2032.13%20us%20%7C%20%20%201%2C015.2%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2025.55%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%208%20%7C%20%20%20%20%20737.8%20us%20%7C%20%2044.23%20us%20%7C%20%2059.05%20us%20%7C%20%20%20%20%20727.5%20us%20%7C%20%200.72%20%7C%20%20%20%200.04%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2056.98%20KB%20%7C%20%20%20%20%20%20%20%202.23%20%7C%5Cn%7C%20WithJson%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%208%20%7C%20%20%20%20%20641.8%20us%20%7C%20%2034.63%20us%20%7C%20%2046.23%20us%20%7C%20%20%20%20%20640.1%20us%20%7C%20%200.62%20%7C%20%20%20%200.04%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2043.64%20KB%20%7C%20%20%20%20%20%20%20%201.71%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%2032%20%7C%20%20%201%2C692.5%20us%20%7C%20%2023.43%20us%20%7C%20%2031.27%20us%20%7C%20%20%201%2C684.7%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2041.84%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%2032%20%7C%20%20%201%2C016.7%20us%20%7C%20%2056.75%20us%20%7C%20%2075.76%20us%20%7C%20%20%20%20%20976.6%20us%20%7C%20%200.60%20%7C%20%20%20%200.04%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2060.35%20KB%20%7C%20%20%20%20%20%20%20%201.44%20%7C%5Cn%7C%20WithJson%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%2032%20%7C%20%20%20%20%20871.5%20us%20%7C%20%2039.02%20us%20%7C%20%2052.10%20us%20%7C%20%20%20%20%20843.8%20us%20%7C%20%200.51%20%7C%20%20%20%200.03%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2047.29%20KB%20%7C%20%20%20%20%20%20%20%201.13%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20128%20%7C%20%20%207%2C665.5%20us%20%7C%20%2028.53%20us%20%7C%20%2038.09%20us%20%7C%20%20%207%2C662.0%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20103.65%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20128%20%7C%20%20%202%2C392.2%20us%20%7C%20%2035.64%20us%20%7C%20%2047.57%20us%20%7C%20%20%202%2C379.7%20us%20%7C%20%200.31%20%7C%20%20%20%200.01%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%2074.85%20KB%20%7C%20%20%20%20%20%20%20%200.72%20%7C%5Cn%7C%20WithJson%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20128%20%7C%20%20%202%2C063.6%20us%20%7C%20%2026.61%20us%20%7C%20%2035.53%20us%20%7C%20%20%202%2C063.5%20us%20%7C%20%200.27%20%7C%20%20%20%200.01%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%2061.2%20KB%20%7C%20%20%20%20%20%20%20%200.59%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20512%20%7C%20%2026%2C444.7%20us%20%7C%20102.44%20us%20%7C%20136.75%20us%20%7C%20%2026%2C421.0%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20343.51%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20512%20%7C%20%20%208%2C134.2%20us%20%7C%20%2032.51%20us%20%7C%20%2043.41%20us%20%7C%20%20%208%2C125.8%20us%20%7C%20%200.31%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20132.34%20KB%20%7C%20%20%20%20%20%20%20%200.39%20%7C%5Cn%7C%20WithJson%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%20%20512%20%7C%20%20%207%2C210.9%20us%20%7C%20%2033.10%20us%20%7C%20%2044.18%20us%20%7C%20%20%207%2C199.6%20us%20%7C%20%200.27%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20116.42%20KB%20%7C%20%20%20%20%20%20%20%200.34%20%7C%5Cn%7C%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%20%20%20%20%20%20%20%20%20%20%20%20%7C%5Cn%7C%20%20Without%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%202048%20%7C%20112%2C512.8%20us%20%7C%20443.78%20us%20%7C%20592.43%20us%20%7C%20112%2C461.1%20us%20%7C%20%201.00%20%7C%20%20%20%200.00%20%7C%205.0000%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%201310.32%20KB%20%7C%20%20%20%20%20%20%20%201.00%20%7C%5Cn%7C%20%20WithXml%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%202048%20%7C%20%2032%2C080.3%20us%20%7C%20138.18%20us%20%7C%20184.47%20us%20%7C%20%2032%2C075.1%20us%20%7C%20%200.29%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20361.05%20KB%20%7C%20%20%20%20%20%20%20%200.28%20%7C%5Cn%7C%20WithJson%20%7C%20String%20%7C%20%20%20%20%20%20%20%20%20%20%202048%20%7C%20%2028%2C929.1%20us%20%7C%20%2084.67%20us%20%7C%20113.03%20us%20%7C%20%2028%2C917.8%20us%20%7C%20%200.26%20%7C%20%20%20%200.00%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20%20%20%20%20-%20%7C%20%20336.47%20KB%20%7C%20%20%20%20%20%20%20%200.26%20%7C%5Cn%22%2C%22settings%22%3A%7B%22display%22%3A%22Duration%22%2C%22scale%22%3A%22Log2%22%2C%22theme%22%3A%22Dark%22%7D%7D "Click for interactive chart" \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 3ba69bd..dd2b4f7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,7 +17,7 @@ The supported types for `T` are: For a detailed explanation of the problem solved by QueryableValues, please continue reading [here][readme-background]. -> 💡 QueryableValues boasts over 120 integration tests that are executed on every supported version of EF. These tests ensure reliability and compatibility, giving you added confidence. +> ✅ QueryableValues boasts over 120 integration tests that are executed on every supported version of EF. These tests ensure reliability and compatibility, giving you added confidence. > 💡 Still on Entity Framework 6 (non-core)? Then [QueryableValues `EF6 Edition`](https://github.com/yv989c/BlazarTech.QueryableValues.EF6) is what you need. @@ -79,6 +79,7 @@ public class Startup } } ``` +> 💡 Pro-tip: `UseQueryableValues` offers an optional `options` delegate for additional configurations. ## How Do You Use It? The `AsQueryableValues` extension method is provided by the `BlazarTech.QueryableValues` namespace; therefore, you must add the following `using` directive to your source code file for it to appear as a method of your [DbContext] instance: From 3903fbee9679adc1dd45b2785cf9c320c88fb0cd Mon Sep 17 00:00:00 2001 From: yv989c Date: Sun, 26 Mar 2023 21:53:36 -0400 Subject: [PATCH 29/29] chore: version bump --- Version.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Version.xml b/Version.xml index 9aebfcd..58cb6c2 100644 --- a/Version.xml +++ b/Version.xml @@ -1,8 +1,8 @@  - 3.6.0 - 5.6.0 - 6.6.0 - 7.1.0 + 3.7.0 + 5.7.0 + 6.7.0 + 7.2.0 \ No newline at end of file