From 98db7937ff6422a75f777c77991c81f2c99630dc Mon Sep 17 00:00:00 2001 From: yv989c Date: Mon, 19 Jun 2023 16:25:52 -0400 Subject: [PATCH 01/12] feat: exposes service registration api Needed to provide a custom service provider via the UseInternalServiceProvider option. --- ...ryableValuesServiceCollectionExtensions.cs | 76 +++++++++++++++++++ .../QueryableValuesSqlServerExtension.cs | 45 +---------- 2 files changed, 77 insertions(+), 44 deletions(-) create mode 100644 src/QueryableValues.SqlServer/QueryableValuesServiceCollectionExtensions.cs diff --git a/src/QueryableValues.SqlServer/QueryableValuesServiceCollectionExtensions.cs b/src/QueryableValues.SqlServer/QueryableValuesServiceCollectionExtensions.cs new file mode 100644 index 0000000..aa3dcc6 --- /dev/null +++ b/src/QueryableValues.SqlServer/QueryableValuesServiceCollectionExtensions.cs @@ -0,0 +1,76 @@ +#if EFCORE +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using System; + +namespace BlazarTech.QueryableValues +{ + /// + /// Provides extension methods to register core QueryableValues services. + /// + public static class QueryableValuesServiceCollectionExtensions + { + /// + /// Adds the services required by QueryableValues for the Microsoft SQL Server database provider. + /// + /// + /// It is only needed when building the internal service provider for use with + /// the method. + /// This is not recommend other than for some advanced scenarios. + /// + /// The to add services to. + /// The same service collection so that multiple calls can be chained. + /// + public static IServiceCollection AddQueryableValuesSqlServer(this IServiceCollection services) + { + if (services is null) + { + throw new ArgumentNullException(nameof(services)); + } + + for (var index = services.Count - 1; index >= 0; index--) + { + var descriptor = services[index]; + if (descriptor.ServiceType != typeof(IModelCustomizer)) + { + continue; + } + + if (descriptor.ImplementationType is null) + { + continue; + } + + // Replace theirs with ours. + services[index] = new ServiceDescriptor( + descriptor.ServiceType, + typeof(ModelCustomizer<>).MakeGenericType(descriptor.ImplementationType), + descriptor.Lifetime + ); + + // Add theirs as is, so we can inject it into ours. + services.Add( + new ServiceDescriptor( + descriptor.ImplementationType, + descriptor.ImplementationType, + descriptor.Lifetime + ) + ); + } + + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + services.TryAddScoped(); + + return services; + } + } +} +#endif \ No newline at end of file diff --git a/src/QueryableValues.SqlServer/QueryableValuesSqlServerExtension.cs b/src/QueryableValues.SqlServer/QueryableValuesSqlServerExtension.cs index 132a330..c541423 100644 --- a/src/QueryableValues.SqlServer/QueryableValuesSqlServerExtension.cs +++ b/src/QueryableValues.SqlServer/QueryableValuesSqlServerExtension.cs @@ -1,8 +1,6 @@ #if EFCORE -using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.DependencyInjection; -using System; using System.Collections.Generic; namespace BlazarTech.QueryableValues @@ -14,48 +12,7 @@ internal sealed class QueryableValuesSqlServerExtension : IDbContextOptionsExten public void ApplyServices(IServiceCollection services) { - if (services is null) - { - throw new ArgumentNullException(nameof(services)); - } - - for (var index = services.Count - 1; index >= 0; index--) - { - var descriptor = services[index]; - if (descriptor.ServiceType != typeof(IModelCustomizer)) - { - continue; - } - - if (descriptor.ImplementationType is null) - { - continue; - } - - // Replace theirs with ours. - services[index] = new ServiceDescriptor( - descriptor.ServiceType, - typeof(ModelCustomizer<>).MakeGenericType(descriptor.ImplementationType), - descriptor.Lifetime - ); - - // Add theirs as is, so we can inject it into ours. - services.Add( - new ServiceDescriptor( - descriptor.ImplementationType, - descriptor.ImplementationType, - descriptor.Lifetime - ) - ); - } - - services.AddSingleton(); - services.AddSingleton(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.AddQueryableValuesSqlServer(); } public void Validate(IDbContextOptions options) From d7e0759d1bb2e6974735e531519d3a598dcce3e9 Mon Sep 17 00:00:00 2001 From: yv989c Date: Mon, 19 Jun 2023 16:29:32 -0400 Subject: [PATCH 02/12] test: custom service provider via UseInternalServiceProvider --- ...onTests.cs => DependencyInjectionTests.cs} | 31 +++- .../Integration/CustomServiceProviderTests.cs | 149 ++++++++++++++++++ 2 files changed, 174 insertions(+), 6 deletions(-) rename tests/QueryableValues.SqlServer.Tests/{QueryableValuesDbContextOptionsExtensionTests.cs => DependencyInjectionTests.cs} (63%) create mode 100644 tests/QueryableValues.SqlServer.Tests/Integration/CustomServiceProviderTests.cs diff --git a/tests/QueryableValues.SqlServer.Tests/QueryableValuesDbContextOptionsExtensionTests.cs b/tests/QueryableValues.SqlServer.Tests/DependencyInjectionTests.cs similarity index 63% rename from tests/QueryableValues.SqlServer.Tests/QueryableValuesDbContextOptionsExtensionTests.cs rename to tests/QueryableValues.SqlServer.Tests/DependencyInjectionTests.cs index 99cfbd6..b9143fb 100644 --- a/tests/QueryableValues.SqlServer.Tests/QueryableValuesDbContextOptionsExtensionTests.cs +++ b/tests/QueryableValues.SqlServer.Tests/DependencyInjectionTests.cs @@ -7,10 +7,19 @@ namespace BlazarTech.QueryableValues.SqlServer.Tests { - public class QueryableValuesDbContextOptionsExtensionTests + public class DependencyInjectionTests { + private void MustReplaceServiceAssertion(IServiceProvider serviceProvider) + { + var ourModelCustomizer = serviceProvider.GetService(); + Assert.IsType>(ourModelCustomizer); + + var theirsModelCustomizer = serviceProvider.GetService(); + Assert.IsType(theirsModelCustomizer); + } + [Fact] - public void MustReplaceService() + public void MustReplaceServiceViaApplyServices() { var services = new ServiceCollection(); @@ -22,11 +31,21 @@ public void MustReplaceService() var serviceProvider = services.BuildServiceProvider(); - var ourModelCustomizer = serviceProvider.GetService(); - Assert.IsType>(ourModelCustomizer); + MustReplaceServiceAssertion(serviceProvider); + } - var theirsModelCustomizer = serviceProvider.GetService(); - Assert.IsType(theirsModelCustomizer); + [Fact] + public void MustReplaceServiceViaAddQueryableValuesSqlServer() + { + var services = new ServiceCollection(); + + services.AddTransient(); + + services.AddQueryableValuesSqlServer(); + + var serviceProvider = services.BuildServiceProvider(); + + MustReplaceServiceAssertion(serviceProvider); } private class FakeModelCustomizer : IModelCustomizer diff --git a/tests/QueryableValues.SqlServer.Tests/Integration/CustomServiceProviderTests.cs b/tests/QueryableValues.SqlServer.Tests/Integration/CustomServiceProviderTests.cs new file mode 100644 index 0000000..4b7aba5 --- /dev/null +++ b/tests/QueryableValues.SqlServer.Tests/Integration/CustomServiceProviderTests.cs @@ -0,0 +1,149 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Xunit; + +namespace BlazarTech.QueryableValues.SqlServer.Tests.Integration +{ + public class CustomServiceProviderTests + { + public class DummyDbContext : DbContext + { + public DummyDbContext(DbContextOptions options) : base(options) + { + } + } + + private static IServiceProvider BuildGoodInternalServiceProvider() + { + var services = new ServiceCollection() + .AddEntityFrameworkSqlServer() + .AddQueryableValuesSqlServer(); + + return services.BuildServiceProvider(); + } + + private static IServiceProvider BuildBadInternalServiceProvider() + { + var services = new ServiceCollection() + .AddEntityFrameworkSqlServer(); + + return services.BuildServiceProvider(); + } + + private static string GetConnectionString() + { + var databaseName = $"DummyDb{Guid.NewGuid():N}"; + var databaseFilePath = Path.Combine(Path.GetTempPath(), $"{databaseName}.mdf"); + return @$"Server=(localdb)\MSSQLLocalDB;Integrated Security=true;Connection Timeout=190;Database={databaseName};AttachDbFileName={databaseFilePath}"; + } + + private static async Task CleanUpDbAsync(DbContext dbContext) + { + try + { + await dbContext.Database.EnsureDeletedAsync(); + } + catch + { + } + } + + [Fact] + public async Task BadInternalServiceProvider() + { + var internalServiceProvider = BuildBadInternalServiceProvider(); + var connectionString = GetConnectionString(); + var services = new ServiceCollection(); + + services.AddDbContext(builder => + { + builder + .UseInternalServiceProvider(internalServiceProvider) + .UseSqlServer(connectionString, options => + { + options.UseQueryableValues(); + }); + }); + + var serviceProvider = services.BuildServiceProvider(); + var dbContext = serviceProvider.GetRequiredService(); + + try + { + await dbContext.Database.EnsureCreatedAsync(); + + var values = new[] { 1, 2, 3 }; + + var exception = await Assert.ThrowsAnyAsync(async () => + { + await dbContext.AsQueryableValues(values).ToListAsync(); + }); + + Assert.StartsWith("QueryableValues have not been configured for ", exception.Message); + } + finally + { + await CleanUpDbAsync(dbContext); + } + } + +#if !EFCORE3 + [Theory] + [InlineData(SqlServerSerialization.UseJson)] + [InlineData(SqlServerSerialization.UseXml)] + public async Task GoodInternalServiceProviderWithConfiguration(SqlServerSerialization sqlServerSerializationOption) + { + var internalServiceProvider = BuildGoodInternalServiceProvider(); + var connectionString = GetConnectionString(); + var services = new ServiceCollection(); + var logEntries = new List(); + + services.AddDbContext(builder => + { + builder + .UseInternalServiceProvider(internalServiceProvider) + .UseSqlServer(connectionString, options => + { + options.UseQueryableValues(options => + { + options.Serialization(sqlServerSerializationOption); + }); + }) + .LogTo(logEntry => { logEntries.Add(logEntry); }, Microsoft.Extensions.Logging.LogLevel.Information); + }); + + var serviceProvider = services.BuildServiceProvider(); + var dbContext = serviceProvider.GetRequiredService(); + + try + { + await dbContext.Database.EnsureCreatedAsync(); + + var values = new[] { 1, 2, 3 }; + var valuesResult = await dbContext.AsQueryableValues(values).ToListAsync(); + Assert.Equal(values, valuesResult); + + switch (sqlServerSerializationOption) + { + case SqlServerSerialization.UseJson: + Assert.Contains(logEntries, i => i.Contains("FROM OPENJSON(@p0)")); + break; + case SqlServerSerialization.UseXml: + Assert.Contains(logEntries, i => i.Contains("FROM @p0.nodes")); + break; + default: + throw new NotImplementedException(); + } + } + finally + { + await CleanUpDbAsync(dbContext); + } + } +#endif + } +} \ No newline at end of file From 0d8238663a9de7dc04eebf62e8b0f7a21e15f336 Mon Sep 17 00:00:00 2001 From: yv989c Date: Mon, 19 Jun 2023 16:48:08 -0400 Subject: [PATCH 03/12] ci: removes redundant build --- .github/workflows/ci-workflow.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index d1162ba..f02de11 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -1,13 +1,6 @@ # Adapted from: https://github.com/giraffe-fsharp/Giraffe/blob/master/.github/workflows/build.yml name: CI/CD Workflow on: - push: - branches: - - develop - - 'feature/**' - paths: - - 'src/**' - - 'Version.xml' pull_request: paths: - 'src/**' From 73ee083789e55e0bc7cc9d3f0ba9b12372a33db8 Mon Sep 17 00:00:00 2001 From: yv989c Date: Mon, 19 Jun 2023 22:09:46 -0400 Subject: [PATCH 04/12] build: exclude files from project --- src/QueryableValues.SqlServer/QueryableValues.SqlServer.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/QueryableValues.SqlServer/QueryableValues.SqlServer.csproj b/src/QueryableValues.SqlServer/QueryableValues.SqlServer.csproj index 4f1a36d..76590e6 100644 --- a/src/QueryableValues.SqlServer/QueryableValues.SqlServer.csproj +++ b/src/QueryableValues.SqlServer/QueryableValues.SqlServer.csproj @@ -4,5 +4,6 @@ netstandard2.1 false Debug;Release;Test - + false + From 9534dd039beb036c8a2a37bf20d844df97ce64e3 Mon Sep 17 00:00:00 2001 From: yv989c Date: Wed, 21 Jun 2023 23:29:23 -0400 Subject: [PATCH 05/12] feat: enumeration support --- .../EntityPropertyMapping.cs | 17 ++++++++++--- .../EntityPropertyTypeName.cs | 1 + .../IQueryableFactory.cs | 6 ++--- .../QueryableValuesDbContextExtensions.cs | 25 ++++++++++++++++--- ...eryableValuesEnabledDbContextExtensions.cs | 19 +++++++++++--- .../SqlServer/QueryableFactory.cs | 20 ++++++++++++--- 6 files changed, 72 insertions(+), 16 deletions(-) diff --git a/src/QueryableValues.SqlServer/EntityPropertyMapping.cs b/src/QueryableValues.SqlServer/EntityPropertyMapping.cs index 040c110..ebdcbb8 100644 --- a/src/QueryableValues.SqlServer/EntityPropertyMapping.cs +++ b/src/QueryableValues.SqlServer/EntityPropertyMapping.cs @@ -43,18 +43,27 @@ private EntityPropertyMapping(PropertyInfo source, PropertyInfo target, Type nor Source = source; Target = target; NormalizedType = normalizedType; + TypeName = GetTypeName(normalizedType); - if (SimpleTypes.TryGetValue(normalizedType, out EntityPropertyTypeName typeName)) + if (TypeName == EntityPropertyTypeName.Unknown) { - TypeName = typeName; + throw new NotSupportedException($"{source.PropertyType.FullName} is not supported."); + } + } + + public static EntityPropertyTypeName GetTypeName(Type type) + { + if (SimpleTypes.TryGetValue(type, out EntityPropertyTypeName typeName)) + { + return typeName; } else { - throw new NotSupportedException($"{source.PropertyType.FullName} is not supported."); + return EntityPropertyTypeName.Unknown; } } - private static Type GetNormalizedType(Type type) => Nullable.GetUnderlyingType(type) ?? type; + public static Type GetNormalizedType(Type type) => Nullable.GetUnderlyingType(type) ?? type; public static bool IsSimpleType(Type type) { diff --git a/src/QueryableValues.SqlServer/EntityPropertyTypeName.cs b/src/QueryableValues.SqlServer/EntityPropertyTypeName.cs index 531cd64..4111970 100644 --- a/src/QueryableValues.SqlServer/EntityPropertyTypeName.cs +++ b/src/QueryableValues.SqlServer/EntityPropertyTypeName.cs @@ -2,6 +2,7 @@ { internal enum EntityPropertyTypeName { + Unknown, Boolean, Byte, Int16, diff --git a/src/QueryableValues.SqlServer/IQueryableFactory.cs b/src/QueryableValues.SqlServer/IQueryableFactory.cs index 9149700..50055bf 100644 --- a/src/QueryableValues.SqlServer/IQueryableFactory.cs +++ b/src/QueryableValues.SqlServer/IQueryableFactory.cs @@ -1,5 +1,4 @@ -#if EFCORE -using BlazarTech.QueryableValues.Builders; +using BlazarTech.QueryableValues.Builders; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; @@ -21,8 +20,9 @@ internal interface IQueryableFactory IQueryable Create(DbContext dbContext, IEnumerable values, bool isUnicode); IQueryable Create(DbContext dbContext, IEnumerable values, bool isUnicode); IQueryable Create(DbContext dbContext, IEnumerable values); + public IQueryable Create(DbContext dbContext, IEnumerable values) + where TEnum : struct, Enum; IQueryable Create(DbContext dbContext, IEnumerable values, Action>? configure) where TSource : notnull; } } -#endif \ No newline at end of file diff --git a/src/QueryableValues.SqlServer/QueryableValuesDbContextExtensions.cs b/src/QueryableValues.SqlServer/QueryableValuesDbContextExtensions.cs index ce49e9d..5ff7f36 100644 --- a/src/QueryableValues.SqlServer/QueryableValuesDbContextExtensions.cs +++ b/src/QueryableValues.SqlServer/QueryableValuesDbContextExtensions.cs @@ -1,5 +1,4 @@ -#if EFCORE -using BlazarTech.QueryableValues.Builders; +using BlazarTech.QueryableValues.Builders; using BlazarTech.QueryableValues.SqlServer; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -227,6 +226,27 @@ public static IQueryable AsQueryableValues(this DbContext dbContext, IEnum return GetQueryableFactory(dbContext).Create(dbContext, values); } + /// + /// Allows an IEnumerable<Enum> to be composed in an Entity Framework query. + /// + /// + /// The supported underlying types are: , , , and . + /// + /// More info: . + /// + /// + /// The owning the query. + /// The sequence of values to compose. + /// An IQueryable<Enum> that can be composed with other entities in the query. + /// + /// + public static IQueryable AsQueryableValues(this DbContext dbContext, IEnumerable values) + where TEnum : struct, Enum + { + ValidateParameters(dbContext, values); + return GetQueryableFactory(dbContext).Create(dbContext, values); + } + /// /// Allows an to be composed in an Entity Framework query. /// @@ -244,4 +264,3 @@ public static IQueryable AsQueryableValues(this DbContext dbCo } } } -#endif \ No newline at end of file diff --git a/src/QueryableValues.SqlServer/QueryableValuesEnabledDbContextExtensions.cs b/src/QueryableValues.SqlServer/QueryableValuesEnabledDbContextExtensions.cs index 5473cb1..3fd084f 100644 --- a/src/QueryableValues.SqlServer/QueryableValuesEnabledDbContextExtensions.cs +++ b/src/QueryableValues.SqlServer/QueryableValuesEnabledDbContextExtensions.cs @@ -1,5 +1,4 @@ -#if EFCORE -using BlazarTech.QueryableValues.Builders; +using BlazarTech.QueryableValues.Builders; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; @@ -200,6 +199,21 @@ public static IQueryable AsQueryableValues(this IQueryableValuesEnabledDbC return GetDbContext(dbContext).AsQueryableValues(values); } + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static IQueryable AsQueryableValues(this IQueryableValuesEnabledDbContext dbContext, IEnumerable values) + where TEnum : struct, Enum + { + return GetDbContext(dbContext).AsQueryableValues(values); + } + /// /// /// @@ -217,4 +231,3 @@ public static IQueryable AsQueryableValues(this IQueryableValu } } } -#endif \ No newline at end of file diff --git a/src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs b/src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs index 282c932..278c603 100644 --- a/src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs +++ b/src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs @@ -1,5 +1,4 @@ -#if EFCORE -using BlazarTech.QueryableValues.Builders; +using BlazarTech.QueryableValues.Builders; using BlazarTech.QueryableValues.Serializers; using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; @@ -387,6 +386,22 @@ public IQueryable Create(DbContext dbContext, IEnumerable values) return CreateForSimpleType(dbContext, values); } + public IQueryable Create(DbContext dbContext, IEnumerable values) + where TEnum : struct, Enum + { + var enumType = typeof(TEnum); + var normalizedType = EntityPropertyMapping.GetNormalizedType(Enum.GetUnderlyingType(enumType)); + + return EntityPropertyMapping.GetTypeName(normalizedType) switch + { + EntityPropertyTypeName.Int32 => CreateForSimpleType(dbContext, values.Select(i => (int)(object)i)).Select(i => (TEnum)(object)i), + EntityPropertyTypeName.Byte => CreateForSimpleType(dbContext, values.Select(i => (byte)(object)i)).Select(i => (TEnum)(object)i), + EntityPropertyTypeName.Int16 => CreateForSimpleType(dbContext, values.Select(i => (short)(object)i)).Select(i => (TEnum)(object)i), + EntityPropertyTypeName.Int64 => CreateForSimpleType(dbContext, values.Select(i => (long)(object)i)).Select(i => (TEnum)(object)i), + _ => throw new NotSupportedException($"The underlying type of {enumType.FullName} ({normalizedType.FullName}) is not supported.") + }; + } + public IQueryable Create(DbContext dbContext, IEnumerable values, Action>? configure) where TSource : notnull { @@ -470,4 +485,3 @@ public IQueryable Create(DbContext dbContext, IEnumerable Date: Wed, 21 Jun 2023 23:32:30 -0400 Subject: [PATCH 06/12] test: enumeration support --- .../Integration/DbContextFixture.cs | 4 +- .../Integration/MyDbContextBase.cs | 10 + .../Integration/SimpleTypeTests.cs | 174 ++++++++++++++++++ 3 files changed, 187 insertions(+), 1 deletion(-) diff --git a/tests/QueryableValues.SqlServer.Tests/Integration/DbContextFixture.cs b/tests/QueryableValues.SqlServer.Tests/Integration/DbContextFixture.cs index 50ae364..328e444 100644 --- a/tests/QueryableValues.SqlServer.Tests/Integration/DbContextFixture.cs +++ b/tests/QueryableValues.SqlServer.Tests/Integration/DbContextFixture.cs @@ -66,7 +66,8 @@ private async Task Seed() StringUnicodeValue = "你好!", DateTimeValue = dateTimeOffset.DateTime, DateTimeOffsetValue = dateTimeOffset, - GuidValue = Guid.Parse("df2c9bfe-9d83-4331-97ce-2876d5dc6576") + GuidValue = Guid.Parse("df2c9bfe-9d83-4331-97ce-2876d5dc6576"), + EnumValue = TestEnum.Value1000 }, new TestDataEntity { @@ -85,6 +86,7 @@ private async Task Seed() DateTimeValue = DateTime.MaxValue, DateTimeOffsetValue = DateTimeOffset.MaxValue, GuidValue = Guid.Parse("f6379213-750f-42df-91b9-73756f28c4b6"), + EnumValue = TestEnum.Value3, ChildEntity = new List { new ChildEntity() diff --git a/tests/QueryableValues.SqlServer.Tests/Integration/MyDbContextBase.cs b/tests/QueryableValues.SqlServer.Tests/Integration/MyDbContextBase.cs index ee70d03..52011a1 100644 --- a/tests/QueryableValues.SqlServer.Tests/Integration/MyDbContextBase.cs +++ b/tests/QueryableValues.SqlServer.Tests/Integration/MyDbContextBase.cs @@ -133,6 +133,7 @@ public class TestDataEntity public DateTime DateTimeValue { get; set; } public DateTimeOffset DateTimeOffsetValue { get; set; } public Guid GuidValue { get; set; } + public TestEnum EnumValue { get; set; } public ICollection ChildEntity { get; set; } = default!; } @@ -141,5 +142,14 @@ public class ChildEntity public int Id { get; set; } public int TestDataEntityId { get; set; } } + + public enum TestEnum + { + None = 0, + Value1 = 1, + Value2 = 2, + Value3 = 3, + Value1000 = 1000 + } } #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 eede326..f87fcf2 100644 --- a/tests/QueryableValues.SqlServer.Tests/Integration/SimpleTypeTests.cs +++ b/tests/QueryableValues.SqlServer.Tests/Integration/SimpleTypeTests.cs @@ -362,6 +362,110 @@ public async Task MustMatchSequenceOfGuid() Assert.Equal(expected, actual); } + enum MyByteEnum : byte + { + None, + A, + B, + C + } + + enum MyInt16Enum : short + { + None, + A, + B, + C + } + + enum MyInt32Enum + { + None, + A, + B, + C + } + + enum MyInt64Enum : long + { + None, + A, + B, + C + } + + [Fact] + public async Task MustMatchSequenceOfEnumByte() + { + var expected = new[] + { + MyByteEnum.None, + MyByteEnum.A, + MyByteEnum.A, + MyByteEnum.B, + MyByteEnum.C, + MyByteEnum.A + }; + + var actual = await _db.AsQueryableValues(expected).ToListAsync(); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task MustMatchSequenceOfEnumInt16() + { + var expected = new[] + { + MyInt16Enum.None, + MyInt16Enum.A, + MyInt16Enum.A, + MyInt16Enum.B, + MyInt16Enum.C, + MyInt16Enum.A + }; + + var actual = await _db.AsQueryableValues(expected).ToListAsync(); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task MustMatchSequenceOfEnumInt32() + { + var expected = new[] + { + MyInt32Enum.A, + MyInt32Enum.A, + MyInt32Enum.B, + MyInt32Enum.C, + MyInt32Enum.A, + MyInt32Enum.None + }; + + var actual = await _db.AsQueryableValues(expected).ToListAsync(); + + Assert.Equal(expected, actual); + } + + [Fact] + public async Task MustMatchSequenceOfEnumInt64() + { + var expected = new[] + { + MyInt64Enum.A, + MyInt64Enum.A, + MyInt64Enum.B, + MyInt64Enum.C, + MyInt64Enum.A, + MyInt64Enum.None + }; + + var actual = await _db.AsQueryableValues(expected).ToListAsync(); + + Assert.Equal(expected, actual); + } + [Fact] public async Task QueryEntityByte() { @@ -662,6 +766,27 @@ select i.Id Assert.Equal(expected, actual); } + [Fact] + public async Task QueryEntityEnum() + { + var values = new[] { + TestEnum.None, + TestEnum.Value1000 + }; + + var expected = new[] { 1, 2 }; + + var actual = await ( + from i in _db.TestData + join v in _db.AsQueryableValues(values) on i.EnumValue equals v + orderby i.Id + select i.Id + ) + .ToArrayAsync(); + + Assert.Equal(expected, actual); + } + [Fact] public async Task MustBeEmpty() @@ -694,6 +819,29 @@ async Task AssertEmpty() } } + [Fact] + public async Task MustBeEmptyEnum() + { + var testCounter = 0; + + await AssertEmpty(); + await AssertEmpty(); + await AssertEmpty(); + await AssertEmpty(); + + // Coverage check. + var expectedTestCount = 4; + Assert.Equal(expectedTestCount, testCounter); + + async Task AssertEmpty() + where T : struct, Enum + { + testCounter++; + var actual = await _db.AsQueryableValues(Array.Empty()).ToListAsync(); + Assert.Empty(actual); + } + } + [Fact] public async Task MustMatchCount() { @@ -729,6 +877,32 @@ async Task AssertCount(Func getValue) } } + [Fact] + public async Task MustMatchCountEnum() + { + const int expectedItemCount = 2500; + + var testCounter = 0; + + await AssertCount(i => (MyByteEnum)(i % 4)); + await AssertCount(i => (MyInt16Enum)(i % 4)); + await AssertCount(i => (MyInt32Enum)(i % 4)); + await AssertCount(i => (MyInt64Enum)(i % 4)); + + // Coverage check. + var expectedTestCount = 4; + Assert.Equal(expectedTestCount, testCounter); + + async Task AssertCount(Func getValue) + where T : struct, Enum + { + testCounter++; + var values = Enumerable.Range(0, expectedItemCount).Select(i => getValue(i)); + var actualItemCount = await _db.AsQueryableValues(values).CountAsync(); + Assert.Equal(expectedItemCount, actualItemCount); + } + } + [Fact] public async Task JoinWithInclude() { From 16af15ffed11be1ecfffa36143502d9a5af7ec24 Mon Sep 17 00:00:00 2001 From: yv989c Date: Wed, 21 Jun 2023 23:33:58 -0400 Subject: [PATCH 07/12] chore: version bump --- Version.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Version.xml b/Version.xml index a7e189b..583758b 100644 --- a/Version.xml +++ b/Version.xml @@ -1,8 +1,8 @@  - 3.8.0 - 5.8.0 - 6.8.0 - 7.3.0 + 3.9.0 + 5.9.0 + 6.9.0 + 7.4.0 \ No newline at end of file From 25c8c6e547cdbb0fee7ef27bee4b889da865773b Mon Sep 17 00:00:00 2001 From: yv989c Date: Fri, 23 Jun 2023 18:40:39 -0400 Subject: [PATCH 08/12] feat: enumeration support (complex type) --- .../EntityPropertyMapping.cs | 60 +++++++++++++++++-- .../Serializers/JsonSerializer.cs | 8 ++- .../Serializers/XmlSerializer.cs | 8 ++- .../SqlServer/QueryableFactory.cs | 2 +- 4 files changed, 70 insertions(+), 8 deletions(-) diff --git a/src/QueryableValues.SqlServer/EntityPropertyMapping.cs b/src/QueryableValues.SqlServer/EntityPropertyMapping.cs index ebdcbb8..1155210 100644 --- a/src/QueryableValues.SqlServer/EntityPropertyMapping.cs +++ b/src/QueryableValues.SqlServer/EntityPropertyMapping.cs @@ -17,6 +17,7 @@ internal sealed class EntityPropertyMapping public PropertyInfo Target { get; } public Type NormalizedType { get; } public EntityPropertyTypeName TypeName { get; } + public bool IsSourceEnum { get; } static EntityPropertyMapping() { @@ -38,12 +39,13 @@ static EntityPropertyMapping() }; } - private EntityPropertyMapping(PropertyInfo source, PropertyInfo target, Type normalizedType) + private EntityPropertyMapping(PropertyInfo source, PropertyInfo target, Type normalizedType, bool isSourceEnum) { Source = source; Target = target; NormalizedType = normalizedType; TypeName = GetTypeName(normalizedType); + IsSourceEnum = isSourceEnum; if (TypeName == EntityPropertyTypeName.Unknown) { @@ -63,7 +65,21 @@ public static EntityPropertyTypeName GetTypeName(Type type) } } - public static Type GetNormalizedType(Type type) => Nullable.GetUnderlyingType(type) ?? type; + public static Type GetNormalizedType(Type type, out bool isEnum) + { + type = Nullable.GetUnderlyingType(type) ?? type; + + isEnum = type.IsEnum; + + if (isEnum) + { + type = Enum.GetUnderlyingType(type); + } + + return type; + } + + public static Type GetNormalizedType(Type type) => GetNormalizedType(type, out _); public static bool IsSimpleType(Type type) { @@ -96,9 +112,9 @@ select g foreach (var sourceProperty in sourceProperties) { - var propertyType = GetNormalizedType(sourceProperty.PropertyType); + var sourcePropertyNormalizedType = GetNormalizedType(sourceProperty.PropertyType, out var isSourceEnum); - if (targetPropertiesByType.TryGetValue(propertyType, out Queue? targetProperties)) + if (targetPropertiesByType.TryGetValue(sourcePropertyNormalizedType, out Queue? targetProperties)) { if (targetProperties.Count == 0) { @@ -108,7 +124,8 @@ select g var mapping = new EntityPropertyMapping( sourceProperty, targetProperties.Dequeue(), - propertyType + sourcePropertyNormalizedType, + isSourceEnum ); mappings.Add(mapping); @@ -128,5 +145,38 @@ public static IReadOnlyList GetMappings() { return GetMappings(typeof(T)); } + + public object? GetSourceNormalizedValue(object objectInstance) + { + var value = Source.GetValue(objectInstance); + + if (value is null) + { + return null; + } + + if (IsSourceEnum) + { + switch (TypeName) + { + case EntityPropertyTypeName.Int32: + value = (int)value; + break; + case EntityPropertyTypeName.Byte: + value = (byte)value; + break; + case EntityPropertyTypeName.Int16: + value = (short)value; + break; + case EntityPropertyTypeName.Int64: + value = (long)value; + break; + default: + throw new NotSupportedException($"The underlying type of {NormalizedType.FullName} ({Enum.GetUnderlyingType(NormalizedType).FullName}) is not supported."); + } + } + + return value; + } } } \ No newline at end of file diff --git a/src/QueryableValues.SqlServer/Serializers/JsonSerializer.cs b/src/QueryableValues.SqlServer/Serializers/JsonSerializer.cs index 4c3f34c..cd53d63 100644 --- a/src/QueryableValues.SqlServer/Serializers/JsonSerializer.cs +++ b/src/QueryableValues.SqlServer/Serializers/JsonSerializer.cs @@ -166,7 +166,13 @@ public void WriteValue(Utf8JsonWriter writer, object entity) { if (_writeValue != null) { - var value = Mapping.Source.GetValue(entity); + var value = Mapping.GetSourceNormalizedValue(entity); + + if (value is null) + { + return; + } + _writeValue.Invoke(writer, value); } } diff --git a/src/QueryableValues.SqlServer/Serializers/XmlSerializer.cs b/src/QueryableValues.SqlServer/Serializers/XmlSerializer.cs index 1b509e2..11de796 100644 --- a/src/QueryableValues.SqlServer/Serializers/XmlSerializer.cs +++ b/src/QueryableValues.SqlServer/Serializers/XmlSerializer.cs @@ -242,7 +242,13 @@ public void WriteValue(XmlWriter writer, object entity) { if (_writeValue != null) { - var value = Mapping.Source.GetValue(entity); + var value = Mapping.GetSourceNormalizedValue(entity); + + if (value is null) + { + return; + } + _writeValue.Invoke(writer, value); } } diff --git a/src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs b/src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs index 278c603..0b2605e 100644 --- a/src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs +++ b/src/QueryableValues.SqlServer/SqlServer/QueryableFactory.cs @@ -390,7 +390,7 @@ public IQueryable Create(DbContext dbContext, IEnumerable v where TEnum : struct, Enum { var enumType = typeof(TEnum); - var normalizedType = EntityPropertyMapping.GetNormalizedType(Enum.GetUnderlyingType(enumType)); + var normalizedType = EntityPropertyMapping.GetNormalizedType(enumType); return EntityPropertyMapping.GetTypeName(normalizedType) switch { From 83a5842399ad3aeebb119fec988515a5ceaab4b2 Mon Sep 17 00:00:00 2001 From: yv989c Date: Fri, 23 Jun 2023 18:40:56 -0400 Subject: [PATCH 09/12] test: enumeration support (complex type) --- .../Integration/ComplexTypeTests.cs | 90 +++++++++++++++++-- 1 file changed, 84 insertions(+), 6 deletions(-) diff --git a/tests/QueryableValues.SqlServer.Tests/Integration/ComplexTypeTests.cs b/tests/QueryableValues.SqlServer.Tests/Integration/ComplexTypeTests.cs index 039aec0..8d92cfe 100644 --- a/tests/QueryableValues.SqlServer.Tests/Integration/ComplexTypeTests.cs +++ b/tests/QueryableValues.SqlServer.Tests/Integration/ComplexTypeTests.cs @@ -13,7 +13,7 @@ public abstract class ComplexTypeTests { protected readonly IMyDbContext _db; - public class TestType + class TestType { public bool BooleanValue { get; set; } public bool? BooleanNullableValue { get; set; } @@ -37,6 +37,14 @@ public class TestType public DateTimeOffset? DateTimeOffsetNullableValue { get; set; } public Guid GuidValue { get; set; } public Guid? GuidNullableValue { get; set; } + public ByteEnum ByteEnumValue { get; set; } + public ByteEnum? ByteEnumNullableValue { get; set; } + public Int16Enum Int16EnumValue { get; set; } + public Int16Enum? Int16EnumNullableValue { get; set; } + public Int32Enum Int32EnumValue { get; set; } + public Int32Enum? Int32EnumNullableValue { get; set; } + public Int64Enum Int64EnumValue { get; set; } + public Int64Enum? Int64EnumNullableValue { get; set; } public char CharValue { get; set; } public char? CharNullableValue { get; set; } public char CharUnicodeValue { get; set; } @@ -53,6 +61,38 @@ public struct TestEntityStruct public string Greeting { get; set; } } + enum ByteEnum : byte + { + None, + A, + B, + C + } + + enum Int16Enum : short + { + None, + A, + B, + C + } + + enum Int32Enum + { + None, + A, + B, + C + } + + enum Int64Enum : long + { + None, + A, + B, + C + } + public ComplexTypeTests(DbContextFixture contextFixture) { _db = contextFixture.Db; @@ -236,7 +276,15 @@ public async Task MustMatchSequenceOfTestTypeUsesConfiguration() CharValue = 'a', CharUnicodeValue = '☢', StringValue = "Lorem ipsum dolor sit amet", - StringUnicodeValue = "😀👋" + StringUnicodeValue = "😀👋", + ByteEnumValue = ByteEnum.A, + ByteEnumNullableValue = ByteEnum.B, + Int16EnumValue = Int16Enum.A, + Int16EnumNullableValue = Int16Enum.B, + Int32EnumValue = Int32Enum.A, + Int32EnumNullableValue =Int32Enum.B, + Int64EnumValue = Int64Enum.A, + Int64EnumNullableValue = Int64Enum.B }, new TestType { @@ -258,13 +306,21 @@ public async Task MustMatchSequenceOfTestTypeUsesConfiguration() CharNullableValue = '1', CharUnicodeValue = '☃', CharUnicodeNullableValue = '☢', - StringValue = "" + StringValue = "", + ByteEnumValue = ByteEnum.None, + Int16EnumValue = Int16Enum.A, + Int32EnumValue = Int32Enum.B, + Int64EnumValue = Int64Enum.C }, new TestType { DecimalValue = 0.00M, CharValue = ' ', - CharUnicodeValue = ' ' + CharUnicodeValue = ' ', + ByteEnumNullableValue = ByteEnum.None, + Int16EnumNullableValue = Int16Enum.A, + Int32EnumNullableValue =Int32Enum.B, + Int64EnumNullableValue = Int64Enum.C } }; @@ -600,6 +656,28 @@ orderby td.Id Assert.Equal(expected, actual); } + [Fact] + public async Task JoinWithEnumInt32() + { + var values = new[] + { + new { Id = 1, Value = TestEnum.None }, + new { Id = 3, Value = TestEnum.Value3 } + }; + + var expected = new[] { 1, 3 }; + + var query = + from td in _db.TestData + join v in _db.AsQueryableValues(values) on new { td.Id, Value = td.EnumValue } equals new { v.Id, v.Value } + orderby td.Id + select td.Id; + + var actual = await query.ToListAsync(); + + Assert.Equal(expected, actual); + } + [Fact] public async Task JoinWithChar() { @@ -812,12 +890,12 @@ orderby td.Id select td; var actual = await query.ToListAsync(); - + Assert.Equal(2, actual.Count); Assert.Equal(1, actual[0].Id); Assert.Equal(2, actual[0].ChildEntity.Count); - + Assert.Equal(3, actual[1].Id); Assert.Equal(1, actual[1].ChildEntity.Count); } From cdde4bc74d0e1813425e3e18fe994b21234e62e6 Mon Sep 17 00:00:00 2001 From: yv989c Date: Fri, 23 Jun 2023 18:47:51 -0400 Subject: [PATCH 10/12] docs: adds Enum to the supported simple types --- README.md | 3 ++- docs/README.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4a3c3aa..2186a15 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ Below are a few examples composing a query using the values provided by an [IEnu ### Simple Type Examples -> 💡 Supports [Byte], [Int16], [Int32], [Int64], [Decimal], [Single], [Double], [DateTime], [DateTimeOffset], [Guid], [Char], and [String]. +> 💡 Supports [Byte], [Int16], [Int32], [Int64], [Decimal], [Single], [Double], [DateTime], [DateTimeOffset], [Guid], [Char], [String], and [Enum]. Using the [Contains][ContainsQueryable] LINQ method: @@ -474,6 +474,7 @@ PRs are welcome! 🙂 [Guid]: https://docs.microsoft.com/en-us/dotnet/api/system.guid [Char]: https://docs.microsoft.com/en-us/dotnet/api/system.char [String]: https://docs.microsoft.com/en-us/dotnet/api/system.string +[Enum]: https://docs.microsoft.com/en-us/dotnet/api/system.enum [BuyMeACoffee]: https://www.buymeacoffee.com/yv989c [BuyMeACoffeeButton]: /docs/images/bmc-48.svg [Repository]: https://github.com/yv989c/BlazarTech.QueryableValues diff --git a/docs/README.md b/docs/README.md index e94921c..0306576 100644 --- a/docs/README.md +++ b/docs/README.md @@ -88,7 +88,7 @@ Below are a few examples composing a query using the values provided by an [IEnu ### Simple Type Examples -> 💡 Supports [Byte], [Int16], [Int32], [Int64], [Decimal], [Single], [Double], [DateTime], [DateTimeOffset], [Guid], [Char], and [String]. +> 💡 Supports [Byte], [Int16], [Int32], [Int64], [Decimal], [Single], [Double], [DateTime], [DateTimeOffset], [Guid], [Char], [String], and [Enum]. Using the [Contains][ContainsQueryable] LINQ method: @@ -218,6 +218,7 @@ Please take a look at the [repository][Repository]. [Guid]: https://docs.microsoft.com/en-us/dotnet/api/system.guid [Char]: https://docs.microsoft.com/en-us/dotnet/api/system.char [String]: https://docs.microsoft.com/en-us/dotnet/api/system.string +[Enum]: https://docs.microsoft.com/en-us/dotnet/api/system.enum [BuyMeACoffee]: https://www.buymeacoffee.com/yv989c [BuyMeACoffeeButton]: https://raw.githubusercontent.com/yv989c/BlazarTech.QueryableValues/develop/docs/images/bmc-48.svg [Repository]: https://github.com/yv989c/BlazarTech.QueryableValues \ No newline at end of file From e2143b9f5ac103ade7b264884b03e46ffca6704c Mon Sep 17 00:00:00 2001 From: yv989c Date: Sat, 24 Jun 2023 19:02:05 -0400 Subject: [PATCH 11/12] test: enumeration support (simple type) --- .../Integration/SimpleTypeTests.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/QueryableValues.SqlServer.Tests/Integration/SimpleTypeTests.cs b/tests/QueryableValues.SqlServer.Tests/Integration/SimpleTypeTests.cs index f87fcf2..bcc072e 100644 --- a/tests/QueryableValues.SqlServer.Tests/Integration/SimpleTypeTests.cs +++ b/tests/QueryableValues.SqlServer.Tests/Integration/SimpleTypeTests.cs @@ -785,6 +785,16 @@ select i.Id .ToArrayAsync(); Assert.Equal(expected, actual); + + actual = await ( + from i in _db.TestData + where _db.AsQueryableValues(values).Contains(i.EnumValue) + orderby i.Id + select i.Id + ) + .ToArrayAsync(); + + Assert.Equal(expected, actual); } From 402eee662ee74f9b42d9e3963bb56a2a88687a28 Mon Sep 17 00:00:00 2001 From: yv989c Date: Sat, 24 Jun 2023 19:05:21 -0400 Subject: [PATCH 12/12] test: refactoring (renaming) --- .../Integration/SimpleTypeTests.cs | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/tests/QueryableValues.SqlServer.Tests/Integration/SimpleTypeTests.cs b/tests/QueryableValues.SqlServer.Tests/Integration/SimpleTypeTests.cs index bcc072e..46caaa4 100644 --- a/tests/QueryableValues.SqlServer.Tests/Integration/SimpleTypeTests.cs +++ b/tests/QueryableValues.SqlServer.Tests/Integration/SimpleTypeTests.cs @@ -362,7 +362,7 @@ public async Task MustMatchSequenceOfGuid() Assert.Equal(expected, actual); } - enum MyByteEnum : byte + enum ByteEnum : byte { None, A, @@ -370,7 +370,7 @@ enum MyByteEnum : byte C } - enum MyInt16Enum : short + enum Int16Enum : short { None, A, @@ -378,7 +378,7 @@ enum MyInt16Enum : short C } - enum MyInt32Enum + enum Int32Enum { None, A, @@ -386,7 +386,7 @@ enum MyInt32Enum C } - enum MyInt64Enum : long + enum Int64Enum : long { None, A, @@ -399,12 +399,12 @@ public async Task MustMatchSequenceOfEnumByte() { var expected = new[] { - MyByteEnum.None, - MyByteEnum.A, - MyByteEnum.A, - MyByteEnum.B, - MyByteEnum.C, - MyByteEnum.A + ByteEnum.None, + ByteEnum.A, + ByteEnum.A, + ByteEnum.B, + ByteEnum.C, + ByteEnum.A }; var actual = await _db.AsQueryableValues(expected).ToListAsync(); @@ -417,12 +417,12 @@ public async Task MustMatchSequenceOfEnumInt16() { var expected = new[] { - MyInt16Enum.None, - MyInt16Enum.A, - MyInt16Enum.A, - MyInt16Enum.B, - MyInt16Enum.C, - MyInt16Enum.A + Int16Enum.None, + Int16Enum.A, + Int16Enum.A, + Int16Enum.B, + Int16Enum.C, + Int16Enum.A }; var actual = await _db.AsQueryableValues(expected).ToListAsync(); @@ -435,12 +435,12 @@ public async Task MustMatchSequenceOfEnumInt32() { var expected = new[] { - MyInt32Enum.A, - MyInt32Enum.A, - MyInt32Enum.B, - MyInt32Enum.C, - MyInt32Enum.A, - MyInt32Enum.None + Int32Enum.A, + Int32Enum.A, + Int32Enum.B, + Int32Enum.C, + Int32Enum.A, + Int32Enum.None }; var actual = await _db.AsQueryableValues(expected).ToListAsync(); @@ -453,12 +453,12 @@ public async Task MustMatchSequenceOfEnumInt64() { var expected = new[] { - MyInt64Enum.A, - MyInt64Enum.A, - MyInt64Enum.B, - MyInt64Enum.C, - MyInt64Enum.A, - MyInt64Enum.None + Int64Enum.A, + Int64Enum.A, + Int64Enum.B, + Int64Enum.C, + Int64Enum.A, + Int64Enum.None }; var actual = await _db.AsQueryableValues(expected).ToListAsync(); @@ -834,10 +834,10 @@ public async Task MustBeEmptyEnum() { var testCounter = 0; - await AssertEmpty(); - await AssertEmpty(); - await AssertEmpty(); - await AssertEmpty(); + await AssertEmpty(); + await AssertEmpty(); + await AssertEmpty(); + await AssertEmpty(); // Coverage check. var expectedTestCount = 4; @@ -894,10 +894,10 @@ public async Task MustMatchCountEnum() var testCounter = 0; - await AssertCount(i => (MyByteEnum)(i % 4)); - await AssertCount(i => (MyInt16Enum)(i % 4)); - await AssertCount(i => (MyInt32Enum)(i % 4)); - await AssertCount(i => (MyInt64Enum)(i % 4)); + await AssertCount(i => (ByteEnum)(i % 4)); + await AssertCount(i => (Int16Enum)(i % 4)); + await AssertCount(i => (Int32Enum)(i % 4)); + await AssertCount(i => (Int64Enum)(i % 4)); // Coverage check. var expectedTestCount = 4;