diff --git a/CHANGELOG.md b/CHANGELOG.md index c9bf7b5..be0b77a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,17 +2,17 @@ ### V3.1.2 (2022/07/14) --[#22](https://github.com/linmasaki/KendoNET.DynamicLinq/discussions/22) Rename this repository. +-[#22](https://github.com/luizfbicalho/KendoNET.DynamicLinq/discussions/22) Rename this repository. ### V3.1.1 (2020/11/05) -- [#13](https://github.com/linmasaki/KendoNET.DynamicLinq/issues/13) Fix the issue that filter will throw exception if decimal property is optional. -- [#6](https://github.com/linmasaki/KendoNET.DynamicLinq/issues/6) Add asynchronous method of retrieving data(This feature is still in the experimental stage, not recommend using it on your product). +- [#13](https://github.com/luizfbicalho/KendoNET.DynamicLinq/issues/13) Fix the issue that filter will throw exception if decimal property is optional. +- [#6](https://github.com/luizfbicalho/KendoNET.DynamicLinq/issues/6) Add asynchronous method of retrieving data(This feature is still in the experimental stage, not recommend using it on your product). ### V3.1.0 (2020/02/11) -- [#10](https://github.com/linmasaki/KendoNET.DynamicLinq/issues/10) Fix the issue that the LINQ query with sub-property can't be translated and will be evaluated locally. -- [#12](https://github.com/linmasaki/KendoNET.DynamicLinq/issues/12) Amend the issue that the method `ToDataSourceResult(this IQueryable queryable, DataSourceRequest request)` would ignore the +- [#10](https://github.com/luizfbicalho/KendoNET.DynamicLinq/issues/10) Fix the issue that the LINQ query with sub-property can't be translated and will be evaluated locally. +- [#12](https://github.com/luizfbicalho/KendoNET.DynamicLinq/issues/12) Amend the issue that the method `ToDataSourceResult(this IQueryable queryable, DataSourceRequest request)` would ignore the aggregator parameter. ### V2.2.2 (2019/09/17) @@ -23,13 +23,13 @@ ### V2.2.0 (2019/07/05) - Change the property `Group` of DataSourceResult to `Groups`. -- [#5](https://github.com/linmasaki/KendoNET.DynamicLinq/issues/5) Add new property `Aggregate` to DataSourceRequest. -- [#5](https://github.com/linmasaki/KendoNET.DynamicLinq/issues/5) Fixed getting wrong grouping data in the request using aggregates in grouping configuration. +- [#5](https://github.com/luizfbicalho/KendoNET.DynamicLinq/issues/5) Add new property `Aggregate` to DataSourceRequest. +- [#5](https://github.com/luizfbicalho/KendoNET.DynamicLinq/issues/5) Fixed getting wrong grouping data in the request using aggregates in grouping configuration. ### V2.1.0 (2019/05/16) -- [#3](https://github.com/linmasaki/KendoNET.DynamicLinq/issues/3) Support new filtering operators of `is null`, `is not null`, `is empty`, `is not empty`, `has value`, and `has no value` in grid. -- [#3](https://github.com/linmasaki/KendoNET.DynamicLinq/issues/3) Filtering operators of `is empty`, `is not empty`, `has value`, and `has no value` doesn't support non-string types. +- [#3](https://github.com/luizfbicalho/KendoNET.DynamicLinq/issues/3) Support new filtering operators of `is null`, `is not null`, `is empty`, `is not empty`, `has value`, and `has no value` in grid. +- [#3](https://github.com/luizfbicalho/KendoNET.DynamicLinq/issues/3) Filtering operators of `is empty`, `is not empty`, `has value`, and `has no value` doesn't support non-string types. ### V2.0.2 (2019/04/12) @@ -41,8 +41,8 @@ ### V2.0.0 (2018/09/10) -- [#2](https://github.com/linmasaki/KendoNET.DynamicLinq/issues/2) Support .Net Standard 2.0. +- [#2](https://github.com/luizfbicalho/KendoNET.DynamicLinq/issues/2) Support .Net Standard 2.0. ### V1.0.3 (2017/02/069) -- [#1](https://github.com/linmasaki/KendoNET.DynamicLinq/issues/1) Add `Errors` property in **`DataSourceResult`** class. +- [#1](https://github.com/luizfbicalho/KendoNET.DynamicLinq/issues/1) Add `Errors` property in **`DataSourceResult`** class. diff --git a/KendoNET.DynamicLinq.EFCore/KendoNET.DynamicLinq.EFCore.csproj b/KendoNET.DynamicLinq.EFCore/KendoNET.DynamicLinq.EFCore.csproj new file mode 100644 index 0000000..2a1426c --- /dev/null +++ b/KendoNET.DynamicLinq.EFCore/KendoNET.DynamicLinq.EFCore.csproj @@ -0,0 +1,55 @@ + + + + net8.0 + enable + Nullable + true + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/KendoNET.DynamicLinq.EFCore/QueryableAsyncExtensions.cs b/KendoNET.DynamicLinq.EFCore/QueryableAsyncExtensions.cs new file mode 100644 index 0000000..e2d36f9 --- /dev/null +++ b/KendoNET.DynamicLinq.EFCore/QueryableAsyncExtensions.cs @@ -0,0 +1,150 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace KendoNET.DynamicLinq.EFCore +{ + /// + /// Provides extension methods for asynchronously applying Kendo-style data operations (paging, sorting, filtering, grouping, and aggregation) + /// to sources using Dynamic LINQ and Entity Framework Core. + /// + public static class QueryableAsyncExtensionsAsync + { + /// + /// Applies data processing (paging, sorting and filtering) over IQueryable using Dynamic Linq. + /// + /// The type of the IQueryable. + /// The IQueryable which should be processed. + /// Specifies how many items to take. Configurable via the pageSize setting of the Kendo DataSource. + /// Specifies how many items to skip. + /// Specifies the current sort order. + /// Specifies the current filter. + /// + /// A DataSourceResult object populated from the processed IQueryable. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static Task> ToDataSourceResultAsync(this IQueryable queryable, int take, int skip, IEnumerable sort, Filter filter, CancellationToken ct) + { + return queryable.ToDataSourceResultAsync(take, skip, sort, filter, null, null, ct); + } + + /// + /// Applies data processing (paging, sorting and filtering) over IQueryable using Dynamic Linq. + /// + /// The type of the IQueryable. + /// The IQueryable which should be processed. + /// The DataSourceRequest object containing take, skip, sort, filter, aggregates, and groups data. + /// + /// A DataSourceResult object populated from the processed IQueryable. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static Task> ToDataSourceResultAsync(this IQueryable queryable, DataSourceRequest request, CancellationToken ct) + { + return queryable.ToDataSourceResultAsync(request.Take, request.Skip, request.Sort, request.Filter, request.Aggregate, request.Group, ct); + } + + /// + /// Applies data processing (paging, sorting, filtering and aggregates) over IQueryable using Dynamic Linq. + /// + /// The type of the IQueryable. + /// The IQueryable which should be processed. + /// Specifies how many items to take. Configurable via the pageSize setting of the Kendo DataSource. + /// Specifies how many items to skip. + /// Specifies the current sort order. + /// Specifies the current filter. + /// Specifies the current aggregates. + /// Specifies the current groups. + /// + /// A DataSourceResult object populated from the processed IQueryable. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static async Task> ToDataSourceResultAsync(this IQueryable queryable, + int take, + int skip, + IEnumerable sort, + Filter? filter, + IEnumerable? aggregates, + IEnumerable? group, + CancellationToken ct) + { + var errors = new List(); + + // Filter the data first + queryable = queryable.Filters(filter, errors); + + // Calculate the total number of records (needed for paging) + var total = await queryable.CountAsync(ct); + + // Calculate the aggregates + var aggregate = queryable.Aggregates(aggregates); + queryable = queryable.UpdateQuery(take, skip, sort, group); + + var result = new DataSourceResult + { + Total = total, + Aggregates = aggregate + }; + + // Group By + if (group?.Any() == true) + { + result.Groups = await queryable.GroupByMany(group).AsQueryable().ToListAsync(ct); + } + else + { + result.Data = await queryable.ToListAsync(ct); + } + + // Set errors if any + if (errors.Count > 0) + { + result.Errors = errors; + } + + return result; + } + } +} diff --git a/KendoNET.DynamicLinq.sln b/KendoNET.DynamicLinq.sln index d47e720..3fe152e 100644 --- a/KendoNET.DynamicLinq.sln +++ b/KendoNET.DynamicLinq.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29609.76 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36121.58 d17.14 MinimumVisualStudioVersion = 15.0.26124.0 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KendoNET.DynamicLinq", "src\KendoNET.DynamicLinq.csproj", "{6F75D7FE-0A2C-4586-8152-D25AA61E6CA9}" EndProject @@ -17,6 +17,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Assets", "Assets", "{531E0E README.md = README.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KendoNET.DynamicLinq.EFCore", "KendoNET.DynamicLinq.EFCore\KendoNET.DynamicLinq.EFCore.csproj", "{E71AD039-BF32-48A0-8BA7-14AA09082B52}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -63,6 +65,18 @@ Global {E3FA5C13-FD73-457B-85F2-30D8BA0CCA82}.Release|x64.Build.0 = Release|Any CPU {E3FA5C13-FD73-457B-85F2-30D8BA0CCA82}.Release|x86.ActiveCfg = Release|Any CPU {E3FA5C13-FD73-457B-85F2-30D8BA0CCA82}.Release|x86.Build.0 = Release|Any CPU + {E71AD039-BF32-48A0-8BA7-14AA09082B52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E71AD039-BF32-48A0-8BA7-14AA09082B52}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E71AD039-BF32-48A0-8BA7-14AA09082B52}.Debug|x64.ActiveCfg = Debug|Any CPU + {E71AD039-BF32-48A0-8BA7-14AA09082B52}.Debug|x64.Build.0 = Debug|Any CPU + {E71AD039-BF32-48A0-8BA7-14AA09082B52}.Debug|x86.ActiveCfg = Debug|Any CPU + {E71AD039-BF32-48A0-8BA7-14AA09082B52}.Debug|x86.Build.0 = Debug|Any CPU + {E71AD039-BF32-48A0-8BA7-14AA09082B52}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E71AD039-BF32-48A0-8BA7-14AA09082B52}.Release|Any CPU.Build.0 = Release|Any CPU + {E71AD039-BF32-48A0-8BA7-14AA09082B52}.Release|x64.ActiveCfg = Release|Any CPU + {E71AD039-BF32-48A0-8BA7-14AA09082B52}.Release|x64.Build.0 = Release|Any CPU + {E71AD039-BF32-48A0-8BA7-14AA09082B52}.Release|x86.ActiveCfg = Release|Any CPU + {E71AD039-BF32-48A0-8BA7-14AA09082B52}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -70,4 +84,4 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E645F9BA-8510-4B50-98F7-3740F27727CB} EndGlobalSection -EndGlobal \ No newline at end of file +EndGlobal diff --git a/LICENSE b/LICENSE index c633310..7fff45f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 CoCo Lin +Copyright (c) 2019 luiz bicalho Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index a50e35f..b0ecfc2 100644 --- a/README.md +++ b/README.md @@ -2,22 +2,22 @@ [![Version](https://img.shields.io/nuget/vpre/KendoNET.DynamicLinq.svg)](https://www.nuget.org/packages/KendoNET.DynamicLinq) [![Downloads](https://img.shields.io/nuget/dt/KendoNET.DynamicLinq.svg)](https://www.nuget.org/packages/KendoNET.DynamicLinq) -[![.NET Standard](https://img.shields.io/badge/.NET%20Standard-%3E%3D%201.6-red.svg)](#) +[![.NET 8.0](https://img.shields.io/badge/.NET-8.0-red)](#) ## Description -KendoNET.DynamicLinq implements server paging, filtering, sorting, grouping, and aggregating to Kendo UI via Dynamic Linq for .Net Core App(1.x ~ 3.x). +KendoNET.DynamicLinq implements server paging, filtering, sorting, grouping, and aggregating to Kendo UI via Dynamic Linq for .Net 8. ## Prerequisites -### .Net Core 1 ~ 2 +### .Net 8 Newtonsoft.Json - None -### .Net Core 3 +### .Net 8 System.Text.Json - You must add custom `ObjectToInferredTypesConverter` to your `JsonSerializerOptions` since `System.Text.Json` didn't deserialize inferred type to object properties now, see - the [sample code](https://github.com/linmasaki/KendoNET.DynamicLinq/blob/master/test/KendoNET.DynamicLinq.Tests/CustomJsonSerializerOptions.cs) + the [sample code](https://github.com/luizfbicalho/KendoNET.DynamicLinq/blob/master/test/KendoNET.DynamicLinq.Tests/CustomJsonSerializerOptions.cs) and [reference](https://docs.microsoft.com/en-gb/dotnet/standard/serialization/system-text-json-converters-how-to#deserialize-inferred-types-to-object-properties). ## Usage @@ -165,7 +165,7 @@ public class MyContext : DbContext 2. Switch to project root directory(src\KendoNET.DynamicLinq). 3. Run "dotnet restore" 4. Run "dotnet pack --configuration Release" -5. Add `` to package metadata of nupkg to show repository URL at Nuget +5. Add `` to package metadata of nupkg to show repository URL at Nuget ## Note diff --git a/src/Aggregator.cs b/src/Aggregator.cs index 011df5d..500c139 100644 --- a/src/Aggregator.cs +++ b/src/Aggregator.cs @@ -16,61 +16,88 @@ public class Aggregator /// Gets or sets the name of the aggregated field (property). /// [DataMember(Name = "field")] - public string Field { get; set; } + public string Field { get; set; } = string.Empty; /// /// Gets or sets the aggregate. /// [DataMember(Name = "aggregate")] - public string Aggregate { get; set; } + public string Aggregate { get; set; } = string.Empty; /// /// Get MethodInfo. /// /// Specifies the type of querable data. /// A MethodInfo for field. - public MethodInfo MethodInfo(Type type) + /// + /// + /// + /// + /// + /// + /// + /// + /// + public MethodInfo? MethodInfo(Type type) { - var proptype = type.GetProperty(Field).PropertyType; + var proptype = type.GetProperty(Field)?.PropertyType ?? throw new ArgumentException($"Property '{Field}' not found in type '{type.Name}'."); switch (Aggregate) { case "max": case "min": - return GetMethod(ConvertTitleCase(Aggregate), MinMaxFunc().GetMethodInfo(), 2).MakeGenericMethod(type, proptype); + return GetMethod(ConvertTitleCase(Aggregate), MinMaxFunc().GetMethodInfo(), 2)?.MakeGenericMethod(type, proptype); case "average": case "sum": - return GetMethod(ConvertTitleCase(Aggregate), - ((Func)GetType().GetMethod("SumAvgFunc", BindingFlags.Static | BindingFlags.NonPublic).MakeGenericMethod(proptype).Invoke(null, null)) - .GetMethodInfo(), 1).MakeGenericMethod(type); + return GetMethod(ConvertTitleCase(Aggregate), GetSumAvg(GetType(), proptype).GetMethodInfo(), 1)?.MakeGenericMethod(type); case "count": return GetMethod(ConvertTitleCase(Aggregate), - Nullable.GetUnderlyingType(proptype) != null ? CountNullableFunc().GetMethodInfo() : CountFunc().GetMethodInfo(), 1).MakeGenericMethod(type); + Nullable.GetUnderlyingType(proptype) != null ? CountNullableFunc().GetMethodInfo() : CountFunc().GetMethodInfo(), 1)?.MakeGenericMethod(type); } return null; } + /// + /// Converts the aggregate name to title case. + /// + /// + /// private static string ConvertTitleCase(string str) { - var tokens = str.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries); + var tokens = str.Split([" "], StringSplitOptions.RemoveEmptyEntries); for (var i = 0; i < tokens.Length; i++) { var token = tokens[i]; - tokens[i] = token.Substring(0, 1).ToUpper() + token.Substring(1); + tokens[i] = $"{token.Substring(0, 1).ToUpperInvariant()}{token.Substring(1)}"; } return string.Join(" ", tokens); } - private static MethodInfo GetMethod(string methodName, MethodInfo methodTypes, int genericArgumentsCount) + /// + /// Get MethodInfo from Queryable methods. + /// + /// + /// + /// + /// + /// + /// + /// + private static MethodInfo? GetMethod(string methodName, MethodInfo? methodTypes, int genericArgumentsCount) { + if (methodTypes == null) + { + throw new ArgumentNullException(nameof(methodTypes), "Method types cannot be null."); + } + var methods = from method in typeof(Queryable).GetMethods(BindingFlags.Public | BindingFlags.Static) - let parameters = method.GetParameters() - let genericArguments = method.GetGenericArguments() - where method.Name == methodName && - genericArguments.Length == genericArgumentsCount && - parameters.Select(p => p.ParameterType).SequenceEqual((Type[])methodTypes.Invoke(null, genericArguments)) - select method; + let parameters = method.GetParameters() + let genericArguments = method.GetGenericArguments() + where method.Name == methodName && + genericArguments.Length == genericArgumentsCount && + parameters.Select(p => p.ParameterType).SequenceEqual((Type[])(methodTypes.Invoke(null, genericArguments) ?? Array.Empty())) + select method; return methods.FirstOrDefault(); } @@ -79,9 +106,13 @@ private static Func CountNullableFunc() return CountNullableDelegate; } + /// + /// Count delegate type for nullable types. + /// + /// private static Type[] CountNullableDelegate(Type t) { - return new[] { typeof(IQueryable<>).MakeGenericType(t), typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(t, typeof(bool))) }; + return [typeof(IQueryable<>).MakeGenericType(t), typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(t, typeof(bool)))]; } private static Func CountFunc() @@ -89,19 +120,44 @@ private static Func CountFunc() return CountDelegate; } + /// + /// Returns the type for count delegate. + /// + /// private static Type[] CountDelegate(Type t) { - return new[] { typeof(IQueryable<>).MakeGenericType(t) }; + return [typeof(IQueryable<>).MakeGenericType(t)]; + } + + /// + /// Gthe Sum or Average delegate type. + /// + /// + /// + /// + /// + /// + /// + /// + private static Func GetSumAvg(Type t, Type proptype) + { +#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields + return (Func)(t.GetMethod(nameof(SumAvgFunc), BindingFlags.Static | BindingFlags.NonPublic)?.MakeGenericMethod(proptype).Invoke(null, null) + ?? throw new ArgumentException("Unable to invoke SumAvgFunc.")); +#pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields } private static Func MinMaxFunc() { return MinMaxDelegate; } - + /// + /// Minor Max delegate type. + /// + /// private static Type[] MinMaxDelegate(Type a, Type b) { - return new[] { typeof(IQueryable<>).MakeGenericType(a), typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(a, b)) }; + return [typeof(IQueryable<>).MakeGenericType(a), typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(a, b))]; } private static Func SumAvgFunc() @@ -109,9 +165,13 @@ private static Func SumAvgFunc() return SumAvgDelegate; } + /// + /// Sum or Average delegate type. + /// + /// private static Type[] SumAvgDelegate(Type t) { - return new[] { typeof(IQueryable<>).MakeGenericType(t), typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(t, typeof(TU))) }; + return [typeof(IQueryable<>).MakeGenericType(t), typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(t, typeof(TU)))]; } } } \ No newline at end of file diff --git a/src/DataSourceRequest.cs b/src/DataSourceRequest.cs index a92dad8..9e1eb10 100644 --- a/src/DataSourceRequest.cs +++ b/src/DataSourceRequest.cs @@ -2,36 +2,40 @@ namespace KendoNET.DynamicLinq { + /// + /// Represents a request for data operations such as paging, sorting, filtering, grouping, and aggregation. + /// Used by Kendo UI DataSource to describe the desired data manipulation. + /// public class DataSourceRequest { /// - /// Specifies how many items to take. + /// Gets or sets the number of items to take (page size). /// public int Take { get; set; } /// - /// Specifies how many items to skip. + /// Gets or sets the number of items to skip (used for paging). /// public int Skip { get; set; } /// - /// Specifies the requested sort order. + /// Gets or sets the collection of sort expressions that define the requested sort order. /// - public IEnumerable Sort { get; set; } + public IEnumerable Sort { get; set; } = []; /// - /// Specifies the requested filter. + /// Gets or sets the filter expression that defines the requested filtering. /// - public Filter Filter { get; set; } + public Filter? Filter { get; set; } /// - /// Specifies the requested grouping . + /// Gets or sets the collection of group expressions that define the requested grouping. /// - public IEnumerable Group { get; set; } + public IEnumerable? Group { get; set; } /// - /// Specifies the requested aggregators. + /// Gets or sets the collection of aggregate expressions that define the requested aggregations. /// - public IEnumerable Aggregate { get; set; } + public IEnumerable? Aggregate { get; set; } } } \ No newline at end of file diff --git a/src/DataSourceResult.cs b/src/DataSourceResult.cs index 0441a9c..960b1ba 100644 --- a/src/DataSourceResult.cs +++ b/src/DataSourceResult.cs @@ -1,5 +1,5 @@ using System; -using System.Collections; +using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; @@ -8,23 +8,23 @@ namespace KendoNET.DynamicLinq /// /// Describes the result of Kendo DataSource read operation. /// - [KnownType("GetKnownTypes")] - public class DataSourceResult + [KnownType(nameof(GetKnownTypes))] + public class DataSourceResult { /// /// Represents a single page of processed data. /// - public IEnumerable Data { get; set; } + public IEnumerable Data { get; set; } = []; /// /// Represents a single page of processed grouped data. /// - public IEnumerable Groups { get; set; } + public IEnumerable? Groups { get; set; } = []; /// /// Represents a requested aggregates. /// - public object Aggregates { get; set; } + public object? Aggregates { get; set; } /// /// The total number of records available. @@ -34,16 +34,18 @@ public class DataSourceResult /// /// Represents error information from server-side. /// - public object Errors { get; set; } + public object? Errors { get; set; } /// /// Used by the KnownType attribute which is required for WCF serialization support /// /// + /// + /// private static Type[] GetKnownTypes() { - var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.FullName.StartsWith("DynamicClasses")); - return assembly == null ? new Type[0] : assembly.GetTypes().Where(t => t.Name.StartsWith("DynamicClass")).ToArray(); + var assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a?.FullName?.StartsWith("DynamicClasses", StringComparison.InvariantCulture) ?? false); + return assembly == null ? [] : assembly.GetTypes().Where(t => t.Name.StartsWith("DynamicClass", StringComparison.InvariantCulture)).ToArray(); } } } \ No newline at end of file diff --git a/src/EnumerableExtensions.cs b/src/EnumerableExtensions.cs index 3d504c0..fbafa1c 100644 --- a/src/EnumerableExtensions.cs +++ b/src/EnumerableExtensions.cs @@ -1,13 +1,26 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Linq.Dynamic.Core; namespace KendoNET.DynamicLinq { + /// + /// Provides extension methods for grouping enumerable collections by multiple selectors. + /// public static class EnumerableExtensions { - public static dynamic GroupByMany(this IEnumerable elements, IEnumerable groupSelectors) + /// + /// Groups the elements of a sequence according to multiple group selectors specified as objects. + /// + /// The type of the elements in the source sequence. + /// The sequence of elements to group. + /// A collection of objects that define the grouping fields and aggregates. + /// + /// An where each element represents a group and its subgroups, including aggregate results. + /// + /// Thrown if an arithmetic operation in the grouping or aggregation overflows. + public static IEnumerable GroupByMany(this IEnumerable elements, IEnumerable groupSelectors) { // Create a new list of Kendo Group Selectors var selectors = new List>(groupSelectors.Count()); @@ -29,29 +42,46 @@ public static dynamic GroupByMany(this IEnumerable elements, return elements.GroupByMany(selectors.ToArray()); } - public static dynamic GroupByMany(this IEnumerable elements, params GroupSelector[] groupSelectors) + /// + /// Groups the elements of a sequence according to multiple group selectors specified as objects. + /// + /// The type of the elements in the source sequence. + /// The sequence of elements to group. + /// An array of objects that define the grouping selectors and aggregates. + /// + /// An where each element represents a group and its subgroups, including aggregate results. + /// + /// Thrown if the group selectors are invalid. + /// Thrown if an arithmetic operation in the grouping or aggregation overflows. + public static IEnumerable GroupByMany(this IEnumerable elements, params GroupSelector[] groupSelectors) { if (groupSelectors.Length > 0) { // Get selector var selector = groupSelectors[0]; var nextSelectors = groupSelectors.Skip(1).ToArray(); // Reduce the list recursively until zero - - // Group by and return - return elements.GroupBy(selector.Selector).Select( + // Group by and return + return elements.GroupBy(selector.Selector).Select( g => new GroupResult { Value = g.Key, - Aggregates = QueryableExtensions.Aggregates(g.AsQueryable(), selector.Aggregates), + Aggregates = g.AsQueryable().Aggregates(selector.Aggregates), HasSubgroups = groupSelectors.Length > 1, Count = g.Count(), - Items = g.GroupByMany(nextSelectors), // Recursivly group the next selectors + Items = g.GroupByMany(nextSelectors), // Recursively group the next selectors SelectorField = selector.Field }); } // If there are not more group selectors return data - return elements; + return elements.Select(s => new GroupResult + { + Aggregates = elements.AsQueryable().Aggregates(null), + Count = 1, + HasSubgroups = false, + SelectorField = string.Empty, + Value = s + }); } } } \ No newline at end of file diff --git a/src/Filter.cs b/src/Filter.cs index 50b31e2..ba87d96 100644 --- a/src/Filter.cs +++ b/src/Filter.cs @@ -17,36 +17,36 @@ public class Filter /// Gets or sets the name of the sorted field (property). Set to null if the Filters property is set. /// [DataMember(Name = "field")] - public string Field { get; set; } + public string Field { get; set; } = string.Empty; /// /// Gets or sets the filtering operator. Set to null if the Filters property is set. /// [DataMember(Name = "operator")] - public string Operator { get; set; } + public string Operator { get; set; } = string.Empty; /// /// Gets or sets the filtering value. Set to null if the Filters property is set. /// [DataMember(Name = "value")] - public object Value { get; set; } + public object? Value { get; set; } /// /// Gets or sets the filtering logic. Can be set to "or" or "and". Set to null unless Filters is set. /// [DataMember(Name = "logic")] - public string Logic { get; set; } + public string Logic { get; set; } = string.Empty; /// /// Gets or sets the child filter expressions. Set to null if there are no child expressions. /// [DataMember(Name = "filters")] - public IEnumerable Filters { get; set; } + public IEnumerable? Filters { get; set; } /// /// Mapping of Kendo DataSource filtering operators to Dynamic Linq /// - private static readonly IDictionary Operators = new Dictionary + private static readonly Dictionary Operators = new Dictionary { { "eq", "=" }, { "neq", "!=" }, @@ -69,11 +69,12 @@ public class Filter /// /// These operators only for string type. /// - private static readonly string[] StringOperators = new[] { "startswith", "endswith", "contains", "doesnotcontain", "isempty", "isnotempty", "isnullorempty", "isnotnullorempty" }; + private static readonly string[] StringOperators = ["startswith", "endswith", "contains", "doesnotcontain", "isempty", "isnotempty", "isnullorempty", "isnotnullorempty"]; /// /// Get a flattened list of all child filter expressions. /// + /// public IList All() { var filters = new List(); @@ -81,7 +82,10 @@ public IList All() return filters; } - + /// + /// Collects the filter expressions into a flat list. + /// + /// private void Collect(IList filters) { if (Filters?.Any() == true) @@ -100,64 +104,53 @@ private void Collect(IList filters) /// /// Converts the filter expression to a predicate suitable for Dynamic Linq e.g. "Field1 = @1 and Field2.Contains(@2)" /// + /// /// A list of flattened filters. + /// + /// + /// public string ToExpression(Type type, IList filters) { if (Filters?.Any() == true) { - return "(" + String.Join(" " + Logic + " ", Filters.Select(filter => filter.ToExpression(type, filters)).ToArray()) + ")"; + return "(" + string.Join(" " + Logic + " ", Filters.Select(filter => filter.ToExpression(type, filters)).ToArray()) + ")"; } var currentPropertyType = GetLastPropertyType(type, Field); - if (currentPropertyType != typeof(String) && StringOperators.Contains(Operator)) + if (currentPropertyType != typeof(string) && StringOperators.Contains(Operator)) { - throw new NotSupportedException(string.Format("Operator {0} not support non-string type", Operator)); + throw new NotSupportedException($"Operator {Operator} not support non-string type"); } - int index = filters.IndexOf(this); + var index = filters.IndexOf(this); var comparison = Operators[Operator]; - //switch(Operator) - //{ - // case "doesnotcontain": - // return String.Format("{0} != null && !{0}.{1}(@{2})", Field, comparison, index); - // case "isnull": - // case "isnotnull": - // return String.Format("{0} {1} null", Field, comparison); - // case "isempty": - // case "isnotempty": - // return String.Format("{0} {1} String.Empty", Field, comparison); - // case "isnullorempty": - // case "isnotnullorempty": - // return String.Format("{0}String.IsNullOrEmpty({1})", comparison, Field); - //} - if (Operator == "doesnotcontain") { - return String.Format("{0} != null && !{0}.{1}(@{2})", Field, comparison, index); + return $"{Field} != null && !{Field}.{comparison}(@{index})"; } if (Operator == "isnull" || Operator == "isnotnull") { - return String.Format("{0} {1} null", Field, comparison); + return $"{Field} {comparison} null"; } if (Operator == "isempty" || Operator == "isnotempty") { - return String.Format("{0} {1} String.Empty", Field, comparison); + return $"{Field} {comparison} String.Empty"; } if (Operator == "isnullorempty" || Operator == "isnotnullorempty") { - return String.Format("{0}String.IsNullOrEmpty({1})", comparison, Field); + return $"{comparison}String.IsNullOrEmpty({Field})"; } if (comparison == "StartsWith" || comparison == "EndsWith" || comparison == "Contains") { - return String.Format("{0} != null && {0}.{1}(@{2})", Field, comparison, index); + return $"{Field} != null && {Field}.{comparison}(@{index})"; } - return String.Format("{0} {1} @{2}", Field, comparison, index); + return $"{Field} {comparison} @{index}"; } /// @@ -165,11 +158,15 @@ public string ToExpression(Type type, IList filters) /// /// Parameter expression /// A list of flattened filters. + /// + /// + /// + /// public Expression ToLambdaExpression(ParameterExpression parameter, IList filters) { if (Filters?.Any() == true) { - Expression compositeExpression = null; + Expression? compositeExpression = null; if (Logic == "and") { foreach (var exp in Filters.Select(filter => filter.ToLambdaExpression(parameter, filters)).ToArray()) @@ -188,21 +185,25 @@ public Expression ToLambdaExpression(ParameterExpression parameter, IList(ParameterExpression parameter, IList(ParameterExpression parameter, IList(ParameterExpression parameter, IList(ParameterExpression parameter, IList(ParameterExpression parameter, IList + /// GEt last property type from the path. + /// + /// + /// internal static Type GetLastPropertyType(Type type, string path) { - Type currentType = type; + var currentType = type; /* Searches for the public property with the specified name */ /* Used in versions above 3.1.0 */ - foreach (string propertyName in path.Split('.')) + foreach (var propertyName in path.Split('.')) { - PropertyInfo property = currentType.GetProperty(propertyName); + var property = currentType.GetProperty(propertyName) ?? throw new ArgumentException($"Property '{propertyName}' not found in type '{currentType.Name}'"); currentType = property.PropertyType; } - /* Retrieves all properties defined on the specified type, including inherited, non-public, instance, and static properties */ - /* Used in versions under 2.2.2 */ - //foreach (string propertyName in path.Split('.')) - //{ - // var typeProperties = currentType.GetRuntimeProperties(); - // currentType = typeProperties.FirstOrDefault(f => f.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase))?.PropertyType; - //} - return currentType; } } diff --git a/src/Group.cs b/src/Group.cs index c319353..3137e09 100644 --- a/src/Group.cs +++ b/src/Group.cs @@ -3,9 +3,15 @@ namespace KendoNET.DynamicLinq { + /// + /// Represents a group expression of Kendo DataSource, including sorting and aggregation information. + /// public class Group : Sort { + /// + /// Gets or sets the collection of aggregate expressions to be applied to the group. + /// [DataMember(Name = "aggregates")] - public IEnumerable Aggregates { get; set; } + public IEnumerable Aggregates { get; set; } = []; } } diff --git a/src/GroupResult.cs b/src/GroupResult.cs index 77ff762..c2da64a 100644 --- a/src/GroupResult.cs +++ b/src/GroupResult.cs @@ -1,31 +1,56 @@ -using System.Runtime.Serialization; +using System.Collections.Generic; +using System.Runtime.Serialization; namespace KendoNET.DynamicLinq { // The response format of the group schema : https://docs.telerik.com/kendo-ui/api/javascript/data/datasource/configuration/schema#schemagroups + /// + /// Represents the result of a grouped query, compatible with the Kendo UI DataSource group schema. + /// [DataContract(Name = "groupresult")] public class GroupResult { - // Small letter properties are kendo js properties so please excuse the warnings + /// + /// Gets or sets the value of the group. This is typically the key by which the data is grouped. + /// [DataMember(Name = "value")] - public object Value { get; set; } + public object? Value { get; set; } - public string SelectorField { get; set; } + /// + /// Gets or sets the field name used for grouping. + /// + public string SelectorField { get; set; } = string.Empty; + /// + /// Gets the field name and count in the format "FieldName (Count)". + /// Used by Kendo UI for group display. + /// [DataMember(Name = "field")] public string Field { get { return $"{this.SelectorField} ({this.Count})"; } } + /// + /// Gets or sets the number of items in the group. + /// public int Count { get; set; } + /// + /// Gets or sets the aggregate results for the group. + /// [DataMember(Name = "aggregates")] - public object Aggregates { get; set; } + public object? Aggregates { get; set; } + /// + /// Gets or sets the subgroups or items within this group. + /// [DataMember(Name = "items")] - public dynamic Items { get; set; } + public IEnumerable? Items { get; set; } + /// + /// Gets or sets a value indicating whether this group contains subgroups. + /// [DataMember(Name = "hasSubgroups")] public bool HasSubgroups { get; set; } // true if there are subgroups } diff --git a/src/GroupSelector.cs b/src/GroupSelector.cs index 257618b..265ef75 100644 --- a/src/GroupSelector.cs +++ b/src/GroupSelector.cs @@ -3,10 +3,25 @@ namespace KendoNET.DynamicLinq { + /// + /// Represents a group selector for Kendo DataSource grouping operations. + /// + /// The type of the elements to group. public class GroupSelector { - public Func Selector { get; set; } - public string Field { get; set; } - public IEnumerable Aggregates { get; set; } + /// + /// Gets or sets the selector function used to extract the grouping key from an element. + /// + public Func Selector { get; set; } = _ => new object(); + + /// + /// Gets or sets the name of the field to group by. + /// + public string Field { get; set; } = string.Empty; + + /// + /// Gets or sets the collection of aggregate expressions to apply to each group. + /// + public IEnumerable Aggregates { get; set; } = []; } } diff --git a/src/KendoNET.DynamicLinq.csproj b/src/KendoNET.DynamicLinq.csproj index d9dbbf8..50e4644 100644 --- a/src/KendoNET.DynamicLinq.csproj +++ b/src/KendoNET.DynamicLinq.csproj @@ -1,24 +1,21 @@  - netstandard1.6;netstandard2.0;netstandard2.1 + net8.0 KendoNET.DynamicLinq + enable + Nullable + true - KendoNET.DynamicLinq + KendoNET.luizfbicalho.DynamicLinq Kendo.DynamicLinqCore - CoCo Lin - Kendo.DynamicLinqCore implements server paging, filtering, sorting, grouping and aggregating to Kendo UI via Dynamic Linq for .Net Core App(1.x ~ 3.x). + Luiz Bicalho + Kendo.DynamicLinqCore implements server paging, filtering, sorting, grouping and aggregating to Kendo UI via Dynamic Linq for .Net 8.0. Supported platforms: - - - .NET Standard 1.6 - - .NET Standard 2.0 - - .NET Standard 2.1 - - .NET Core 1.0 ~ .NET Core 1.1 - - .NET Core 2.0 ~ .NET Core 2.2 - - .NET Core 3.0 ~ .NET Core 3.1 + - .NET Core 8.0 @@ -26,28 +23,62 @@ 1. Fix the issue that filter will throw exception if decimal property is optional. 2. Add asynchronous method of retrieving data(This feature is still in the experimental stage, not recommend using it on your product). - Full changelog: https://github.com/linmasaki/Kendo.DynamicLinqCore/blob/master/CHANGELOG.md + Full changelog: https://github.com/luizfbicalho/Kendo.DynamicLinqCore/blob/master/CHANGELOG.md - https://github.com/linmasaki/Kendo.DynamicLinqCore - https://github.com/linmasaki/Kendo.DynamicLinqCore - https://raw.githubusercontent.com/linmasaki/CoCoPackageIcon/master/cocodotnet64.png + https://github.com/luizfbicalho/Kendo.DynamicLinqCore + https://github.com/luizfbicalho/Kendo.DynamicLinqCore + https://raw.githubusercontent.com/luizfbicalho/CoCoPackageIcon/master/cocodotnet64.png Icon.png netcore netstandard kendo kendo-ui linq dynamic 3.1.1 3.1.1 - Copyright © 2017-2020 CoCo Lin + Copyright © 2017-2020 LICENSE - - - + + + - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/QueryableExtensions.cs b/src/QueryableExtensions.cs index ff842d5..2ff2f12 100644 --- a/src/QueryableExtensions.cs +++ b/src/QueryableExtensions.cs @@ -1,13 +1,17 @@ using System; -using System.Threading.Tasks; using System.Collections.Generic; -using System.Reflection; +using System.Globalization; using System.Linq; using System.Linq.Dynamic.Core; using System.Linq.Expressions; +using System.Reflection; namespace KendoNET.DynamicLinq { + /// + /// Provides extension methods for asynchronously applying Kendo-style data operations (paging, sorting, filtering, grouping, and aggregation) + /// to sources using Dynamic LINQ + /// public static class QueryableExtensions { /// @@ -20,7 +24,22 @@ public static class QueryableExtensions /// Specifies the current sort order. /// Specifies the current filter. /// A DataSourceResult object populated from the processed IQueryable. - public static DataSourceResult ToDataSourceResult(this IQueryable queryable, int take, int skip, IEnumerable sort, Filter filter) + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static DataSourceResult ToDataSourceResult(this IQueryable queryable, int take, int skip, IEnumerable sort, Filter filter) { return queryable.ToDataSourceResult(take, skip, sort, filter, null, null); } @@ -32,7 +51,22 @@ public static DataSourceResult ToDataSourceResult(this IQueryable queryabl /// The IQueryable which should be processed. /// The DataSourceRequest object containing take, skip, sort, filter, aggregates, and groups data. /// A DataSourceResult object populated from the processed IQueryable. - public static DataSourceResult ToDataSourceResult(this IQueryable queryable, DataSourceRequest request) + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static DataSourceResult ToDataSourceResult(this IQueryable queryable, DataSourceRequest request) { return queryable.ToDataSourceResult(request.Take, request.Skip, request.Sort, request.Filter, request.Aggregate, request.Group); } @@ -49,13 +83,29 @@ public static DataSourceResult ToDataSourceResult(this IQueryable queryabl /// Specifies the current aggregates. /// Specifies the current groups. /// A DataSourceResult object populated from the processed IQueryable. - public static DataSourceResult ToDataSourceResult(this IQueryable queryable, + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static DataSourceResult ToDataSourceResult(this IQueryable queryable, int take, int skip, IEnumerable sort, - Filter filter, - IEnumerable aggregates, - IEnumerable group) + Filter? filter, + IEnumerable? aggregates, + IEnumerable? group) { var errors = new List(); @@ -66,33 +116,11 @@ public static DataSourceResult ToDataSourceResult(this IQueryable queryabl var total = queryable.Count(); // Calculate the aggregates - var aggregate = Aggregates(queryable, aggregates); - - if (group?.Any() == true) - { - //if(sort == null) sort = GetDefaultSort(queryable.ElementType, sort); - if (sort == null) sort = new List(); - - foreach (var source in group.Reverse()) - { - sort = sort.Append(new Sort - { - Field = source.Field, - Dir = source.Dir - }); - } - } + var aggregate = queryable.Aggregates(aggregates); - // Sort the data - queryable = Sort(queryable, sort); + queryable = queryable.UpdateQuery(take, skip, sort, group); - // Finally page the data - if (take > 0) - { - queryable = Page(queryable, take, skip); - } - - var result = new DataSourceResult + var result = new DataSourceResult { Total = total, Aggregates = aggregate @@ -118,29 +146,41 @@ public static DataSourceResult ToDataSourceResult(this IQueryable queryabl } /// - /// Asynchronously applies data processing (paging, sorting, filtering and aggregates) over IQueryable using Dynamic Linq. + /// Updates the IQueryable with sorting and paging. /// - /// The type of the IQueryable. - /// The IQueryable which should be processed. - /// Specifies how many items to take. Configurable via the pageSize setting of the Kendo DataSource. - /// Specifies how many items to skip. - /// Specifies the current sort order. - /// Specifies the current filter. - /// Specifies the current aggregates. - /// Specifies the current groups. - /// A DataSourceResult object populated from the processed IQueryable. - public static Task ToDataSourceResultAsync(this IQueryable queryable, - int take, - int skip, - IEnumerable sort, - Filter filter, - IEnumerable aggregates = null, - IEnumerable group = null) + /// + public static IQueryable UpdateQuery(this IQueryable queryable, int take, int skip, IEnumerable sort, IEnumerable? group) { - return Task.Run(() => queryable.ToDataSourceResult(take, skip, sort, filter, aggregates, group)); + if (group?.Any() == true) + { + sort ??= []; + foreach (var source in group.Reverse()) + { + sort = sort.Append(new Sort + { + Field = source.Field, + Dir = source.Dir + }); + } + } + + // Sort the data + queryable = queryable.Sort(sort); + + // Finally page the data + if (take > 0) + { + queryable = queryable.Page(take, skip); + } + return queryable; } - private static IQueryable Filters(IQueryable queryable, Filter filter, List errors) + /// + /// Set Filters for IQueryable using Dynamic Linq. + /// + /// + /// + public static IQueryable Filters(this IQueryable queryable, Filter? filter, List errors) { if (filter?.Logic != null) { @@ -168,45 +208,42 @@ private static IQueryable Filters(IQueryable queryable, Filter filter, // Step.3 Use the Where method of Dynamic Linq to filter the data queryable = queryable.Where(predicate, values); - - /* Method.2 Use the combined lambda expression */ - // Step.1 Create a parameter "p" - //var parameter = Expression.Parameter(typeof(T), "p"); - - // Step.2 Make up expression e.g. (p.Number >= 3) AndAlso (p.Company.Name.Contains("M")) - //Expression expression; - //try - //{ - // expression = filter.ToLambdaExpression(parameter, filters); - //} - //catch(Exception ex) - //{ - // errors.Add(ex.Message); - // return queryable; - //} - - // Step.3 The result is e.g. p => (p.Number >= 3) AndAlso (p.Company.Name.Contains("M")) - //var predicateExpression = Expression.Lambda>(expression, parameter); - //queryable = queryable.Where(predicateExpression); } return queryable; } - internal static object Aggregates(IQueryable queryable, IEnumerable aggregates) + /// + /// Agregates the IQueryable using Dynamic Linq. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static object? Aggregates(this IQueryable queryable, IEnumerable? aggregates) { if (aggregates?.Any() == true) { var objProps = new Dictionary(); var groups = aggregates.GroupBy(g => g.Field); - Type type = null; + Type? type = null; foreach (var group in groups) { - var fieldProps = new Dictionary(); + var fieldProps = new Dictionary(); foreach (var aggregate in group) { - var prop = typeof(T).GetProperty(aggregate.Field); + var prop = typeof(T).GetProperty(aggregate.Field) ?? throw new ArgumentException($"Property '{aggregate.Field}' does not exist on type '{typeof(T).Name}'."); var param = Expression.Parameter(typeof(T), "s"); var selector = aggregate.Aggregate == "count" && (Nullable.GetUnderlyingType(prop.PropertyType) != null) ? Expression.Lambda(Expression.NotEqual(Expression.MakeMemberAccess(param, prop), Expression.Constant(null, prop.PropertyType)), param) @@ -215,8 +252,8 @@ internal static object Aggregates(IQueryable queryable, IEnumerable)[queryable.Expression] + : (IEnumerable)[queryable.Expression, Expression.Quote(selector)])); fieldProps.Add(new DynamicProperty(aggregate.Aggregate, typeof(object)), val); } @@ -225,9 +262,12 @@ internal static object Aggregates(IQueryable queryable, IEnumerable(IQueryable queryable, IEnumerable(IQueryable queryable, IEnumerable Sort(IQueryable queryable, IEnumerable sort) + /// + /// Sorts the IQueryable using Dynamic Linq. + /// + /// + public static IQueryable Sort(this IQueryable queryable, IEnumerable sort) { if (sort?.Any() == true) { @@ -259,7 +303,17 @@ private static IQueryable Sort(IQueryable queryable, IEnumerable return queryable; } - private static IQueryable Page(IQueryable queryable, int take, int skip) + /// + /// Applies paging to the by skipping a specified number of elements and then taking a specified number of elements. + /// + /// The type of the elements in the source queryable. + /// The source to page. + /// The number of elements to take (page size). + /// The number of elements to skip (used for paging). + /// + /// An that contains the elements that occur after skipping elements and then taking elements from the input sequence. + /// + public static IQueryable Page(this IQueryable queryable, int take, int skip) { return queryable.Skip(skip).Take(take); } @@ -267,7 +321,9 @@ private static IQueryable Page(IQueryable queryable, int take, int skip /// /// Pretreatment of specific DateTime type and convert some illegal value type /// + /// /// + /// private static Filter PreliminaryWork(Type type, Filter filter) { if (filter.Filters != null && filter.Logic != null) @@ -285,20 +341,14 @@ private static Filter PreliminaryWork(Type type, Filter filter) // When we have a decimal value, it gets converted to an integer/double that will result in the query break var currentPropertyType = Filter.GetLastPropertyType(type, filter.Field); - if ((currentPropertyType == typeof(decimal) || currentPropertyType == typeof(decimal?)) && decimal.TryParse(filter.Value.ToString(), out decimal number)) + if ((currentPropertyType == typeof(decimal) || currentPropertyType == typeof(decimal?)) && decimal.TryParse(filter.Value.ToString(), out var number)) { filter.Value = number; return filter; } - // if(currentPropertyType.GetTypeInfo().IsEnum && int.TryParse(filter.Value.ToString(), out int enumValue)) - // { - // filter.Value = Enum.ToObject(currentPropertyType, enumValue); - // return filter; - // } - // Convert datetime-string to DateTime - if (currentPropertyType == typeof(DateTime) && DateTime.TryParse(filter.Value.ToString(), out DateTime dateTime)) + if (currentPropertyType == typeof(DateTime) && DateTime.TryParse(filter.Value.ToString(), DateTimeFormatInfo.CurrentInfo, out var dateTime)) { filter.Value = dateTime; @@ -311,76 +361,36 @@ private static Filter PreliminaryWork(Type type, Filter filter) if (localTime.Hour != 0 || localTime.Minute != 0 || localTime.Second != 0) return filter; - var newFilter = new Filter { Logic = "and" }; - newFilter.Filters = new List + var newFilter = new Filter { + Logic = "and", + Filters = + [ // Instead of comparing for exact equality, we compare as greater than the start of the day... - new Filter - { - Field = filter.Field, - Filters = filter.Filters, - Value = new DateTime(localTime.Year, localTime.Month, localTime.Day, 0, 0, 0), - Operator = "gte" - }, + new() { + Field = filter.Field, + Filters = filter.Filters??[], + Value = new DateTime(localTime.Year, localTime.Month, localTime.Day, 0, 0, 0,DateTimeKind.Unspecified), + Operator = "gte" + }, // ...and less than the end of that same day (we're making an additional filter here) - new Filter - { - Field = filter.Field, - Filters = filter.Filters, - Value = new DateTime(localTime.Year, localTime.Month, localTime.Day, 23, 59, 59), - Operator = "lte" - } + new() { + Field = filter.Field, + Filters = filter.Filters??[], + Value = new DateTime(localTime.Year, localTime.Month, localTime.Day, 23, 59, 59,DateTimeKind.Unspecified), + Operator = "lte" + } + ] }; return newFilter; } // Convert datetime to local - filter.Value = new DateTime(localTime.Year, localTime.Month, localTime.Day, localTime.Hour, localTime.Minute, localTime.Second, localTime.Millisecond); + filter.Value = new DateTime(localTime.Year, localTime.Month, localTime.Day, localTime.Hour, localTime.Minute, localTime.Second, localTime.Millisecond, DateTimeKind.Unspecified); } return filter; } - - /// - /// The way this extension works it pages the records using skip and takes to do that we need at least one sort property. - /// - private static IEnumerable GetDefaultSort(Type type, IEnumerable sort) - { - if (sort == null) - { - var elementType = type; - var properties = elementType.GetProperties().ToList(); - - //by default make dir desc - var sortByObject = new Sort { Dir = "desc" }; - - PropertyInfo propertyInfo; - //look for property that is called id - if (properties.Any(p => string.Equals(p.Name, "id", StringComparison.OrdinalIgnoreCase))) - { - propertyInfo = properties.FirstOrDefault(p => string.Equals(p.Name, "id", StringComparison.OrdinalIgnoreCase)); - } - //or contains id - else if (properties.Any(p => p.Name.IndexOf("id", StringComparison.OrdinalIgnoreCase) >= 0)) - { - propertyInfo = properties.FirstOrDefault(p => p.Name.IndexOf("id", StringComparison.OrdinalIgnoreCase) >= 0); - } - //or just get the first property - else - { - propertyInfo = properties.FirstOrDefault(); - } - - if (propertyInfo != null) - { - sortByObject.Field = propertyInfo.Name; - } - - sort = new List { sortByObject }; - } - - return sort; - } } } \ No newline at end of file diff --git a/src/Sort.cs b/src/Sort.cs index 6312ac7..deb08ae 100644 --- a/src/Sort.cs +++ b/src/Sort.cs @@ -12,13 +12,13 @@ public class Sort /// Gets or sets the name of the sorted field (property). /// [DataMember(Name = "field")] - public string Field { get; set; } + public string Field { get; set; } = string.Empty; /// /// Gets or sets the sort direction. Should be either "asc" or "desc". /// [DataMember(Name = "dir")] - public string Dir { get; set; } + public string Dir { get; set; } = string.Empty; /// /// Converts to form required by Dynamic Linq e.g. "Field1 desc" diff --git a/test/KendoNET.DynamicLinq.ConsoleApp/KendoNET.DynamicLinq.ConsoleApp.csproj b/test/KendoNET.DynamicLinq.ConsoleApp/KendoNET.DynamicLinq.ConsoleApp.csproj index e4c34a4..fe5bf90 100644 --- a/test/KendoNET.DynamicLinq.ConsoleApp/KendoNET.DynamicLinq.ConsoleApp.csproj +++ b/test/KendoNET.DynamicLinq.ConsoleApp/KendoNET.DynamicLinq.ConsoleApp.csproj @@ -1,17 +1,11 @@ - + Exe - netcoreapp1.0;netcoreapp2.1;netcoreapp3.1 + net8.0 KendoNET.DynamicLinq.ConsoleApp - - - - - - diff --git a/test/KendoNET.DynamicLinq.ConsoleApp/Models/MockData.cs b/test/KendoNET.DynamicLinq.ConsoleApp/Models/MockData.cs index 2877a8e..538ecd4 100644 --- a/test/KendoNET.DynamicLinq.ConsoleApp/Models/MockData.cs +++ b/test/KendoNET.DynamicLinq.ConsoleApp/Models/MockData.cs @@ -1,11 +1,11 @@ -using System; +using System; namespace KendoNET.DynamicLinq.ConsoleApp.Models { public static class MockData { - public static readonly Employee[] Employees = new Employee[] - { + public static readonly Employee[] Employees = + [ new Employee { Number = 10, @@ -71,6 +71,6 @@ public static class MockData Weight = 99.8F, Birthday = new DateTime(2005, 3, 16, 8, 0, 0) } - }; + ]; } } \ No newline at end of file diff --git a/test/KendoNET.DynamicLinq.ConsoleApp/Program.cs b/test/KendoNET.DynamicLinq.ConsoleApp/Program.cs index d63299e..ac412bd 100644 --- a/test/KendoNET.DynamicLinq.ConsoleApp/Program.cs +++ b/test/KendoNET.DynamicLinq.ConsoleApp/Program.cs @@ -21,8 +21,8 @@ static void Main(string[] args) Console.WriteLine("----------------------------------------"); /* Test 1 (Aggregate)*/ - var result = MockData.Employees.AsQueryable().ToDataSourceResult(1, 2, null, null, new[] - { + var result = MockData.Employees.AsQueryable().ToDataSourceResult(1, 2, null, null, + [ new Aggregator { Aggregate = "sum", @@ -33,7 +33,7 @@ static void Main(string[] args) Aggregate = "average", Field = "Salary" } - }, null); + ], null); Console.WriteLine("\r\n/********** Test 1 (Aggregate) **********/"); Console.WriteLine("Expectation: { Salary = { sum = 24750, average = 4125 } }"); @@ -41,14 +41,14 @@ static void Main(string[] args) /* Test 2 (DateTime)*/ - result = MockData.Employees.AsQueryable().ToDataSourceResult(10, 0, new[] - { + result = MockData.Employees.AsQueryable().ToDataSourceResult(10, 0, + [ new Sort { Field = "Name", Dir = "asc" } - }, + ], new Filter { Field = "Birthday", @@ -63,18 +63,18 @@ static void Main(string[] args) /* Test 3 (String Method)*/ - result = MockData.Employees.AsQueryable().ToDataSourceResult(10, 0, new[] - { + result = MockData.Employees.AsQueryable().ToDataSourceResult(10, 0, + [ new Sort { Field = "Name", Dir = "asc" } - }, + ], new Filter { - Filters = new[] - { + Filters = + [ new Filter { Field = "Introduce", @@ -87,7 +87,7 @@ static void Main(string[] args) Operator = "doesnotcontain", Value = "Monie" } - }, + ], Logic = "and" }, null, null); @@ -97,19 +97,19 @@ static void Main(string[] args) /* Test 4 (Double)*/ - result = MockData.Employees.AsQueryable().ToDataSourceResult(10, 0, new[] - { + result = MockData.Employees.AsQueryable().ToDataSourceResult(10, 0, + [ new Sort { Field = "Name", Dir = "asc" } - }, + ], new Filter { Logic = "or", - Filters = new[] - { + Filters = + [ new Filter { Field = "Height", @@ -122,7 +122,7 @@ static void Main(string[] args) Operator = "lte", Value = 166 } - } + ] }, null, null); Console.WriteLine("\r\n/********** Test 4 (Double) **********/"); @@ -131,19 +131,19 @@ static void Main(string[] args) /* Test 5 (Float)*/ - result = MockData.Employees.AsQueryable().ToDataSourceResult(10, 0, new[] - { + result = MockData.Employees.AsQueryable().ToDataSourceResult(10, 0, + [ new Sort { Field = "Name", Dir = "asc" } - }, + ], new Filter { Logic = "and", - Filters = new[] - { + Filters = + [ new Filter { Field = "Weight", @@ -156,7 +156,7 @@ static void Main(string[] args) Operator = "lte", Value = 82.8F } - } + ] }, null, null); Console.WriteLine("\r\n/********** Test 5 (Float) **********/"); diff --git a/test/KendoNET.DynamicLinq.Test/AggregatorTest.cs b/test/KendoNET.DynamicLinq.Test/AggregatorTest.cs index 3456fe7..e69d781 100644 --- a/test/KendoNET.DynamicLinq.Test/AggregatorTest.cs +++ b/test/KendoNET.DynamicLinq.Test/AggregatorTest.cs @@ -1,13 +1,9 @@ -using NUnit.Framework; -using System.Collections.Generic; +using System.Collections.Generic; using KendoNET.DynamicLinq.Test.Data; - -#if NETCOREAPP3_1 -using System.Text.Json; - -#else using Newtonsoft.Json; -#endif +using NUnit.Framework; +using NUnit.Framework.Legacy; + namespace KendoNET.DynamicLinq.Test { @@ -16,19 +12,15 @@ public class AggregatorTest { private MockContext _dbContext; -#if NETCOREAPP3_1 - private static JsonSerializerOptions jsonSerializerOptions = CustomJsonSerializerOptions.DefaultOptions; -#endif + public static IEnumerable DataSourceRequestWithAggregateSalarySum { get { -#if NETCOREAPP3_1 - yield return JsonSerializer.Deserialize("{\"take\":10,\"skip\":0,\"aggregate\":[{\"field\":\"Salary\",\"aggregate\":\"sum\"}]}", jsonSerializerOptions); -#else + yield return JsonConvert.DeserializeObject("{\"take\":10,\"skip\":0,\"aggregate\":[{\"field\":\"Salary\",\"aggregate\":\"sum\"}]}"); -#endif + } } @@ -36,14 +28,10 @@ public static IEnumerable DataSourceRequestWithManyAggregates { get { -#if NETCOREAPP3_1 - yield return JsonSerializer.Deserialize( - "{\"take\":10,\"skip\":0,\"aggregate\":[{\"field\":\"Salary\",\"aggregate\":\"sum\"},{\"field\":\"Salary\",\"aggregate\":\"average\"},{\"field\":\"Number\",\"aggregate\":\"max\"}]}", - jsonSerializerOptions); -#else + yield return JsonConvert.DeserializeObject( "{\"take\":10,\"skip\":0,\"aggregate\":[{\"field\":\"Salary\",\"aggregate\":\"sum\"},{\"field\":\"Salary\",\"aggregate\":\"average\"},{\"field\":\"Number\",\"aggregate\":\"max\"}]}"); -#endif + } } @@ -66,7 +54,7 @@ public void InputParameter_DecimalSum_CheckResultObjectString() }, null); object expectedObject = "{ Salary = { sum = 14850 } }"; - Assert.AreEqual(expectedObject, result.Aggregates.ToString()); + ClassicAssert.AreEqual(expectedObject, result.Aggregates.ToString()); } [TestCaseSource(nameof(DataSourceRequestWithAggregateSalarySum))] @@ -75,7 +63,7 @@ public void InputDataSourceRequest_DecimalSum_CheckResultObjectString(DataSource var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(dataSourceRequest); object expectedObject = "{ Salary = { sum = 14850 } }"; - Assert.AreEqual(expectedObject, result.Aggregates.ToString()); + ClassicAssert.AreEqual(expectedObject, result.Aggregates.ToString()); } [TestCaseSource(nameof(DataSourceRequestWithAggregateSalarySum))] @@ -86,10 +74,10 @@ public void InputDataSourceRequest_DecimalSum_CheckResultSum(DataSourceRequest d var salarySum = salaryAggregates?.GetType().GetProperty("sum")?.GetValue(salaryAggregates, null); const decimal expectedSalarySum = 14850; - Assert.AreEqual(expectedSalarySum, salarySum); + ClassicAssert.AreEqual(expectedSalarySum, salarySum); const decimal incorrectSalarySum = 9999; - Assert.AreNotEqual(incorrectSalarySum, salarySum); + ClassicAssert.AreNotEqual(incorrectSalarySum, salarySum); } [Test] @@ -115,7 +103,7 @@ public void InputParameter_ManyAggregators_CheckResultObjectString() }, null); object expectedObject = "{ Salary = { sum = 14850, average = 2970 }, Number = { max = 6 } }"; - Assert.AreEqual(expectedObject, result.Aggregates.ToString()); + ClassicAssert.AreEqual(expectedObject, result.Aggregates.ToString()); } [TestCaseSource(nameof(DataSourceRequestWithManyAggregates))] @@ -124,7 +112,7 @@ public void InputDataSourceRequest_ManyAggregators_CheckResultObjectString(DataS var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(dataSourceRequest); object expectedObject = "{ Salary = { sum = 14850, average = 2970 }, Number = { max = 6 } }"; - Assert.AreEqual(expectedObject, result.Aggregates.ToString()); + ClassicAssert.AreEqual(expectedObject, result.Aggregates.ToString()); } } } \ No newline at end of file diff --git a/test/KendoNET.DynamicLinq.Test/AggregatorTestSystem.cs b/test/KendoNET.DynamicLinq.Test/AggregatorTestSystem.cs new file mode 100644 index 0000000..f6382e6 --- /dev/null +++ b/test/KendoNET.DynamicLinq.Test/AggregatorTestSystem.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; +using System.Text.Json; +using KendoNET.DynamicLinq.Test.Data; +using NUnit.Framework; +using NUnit.Framework.Legacy; + + +namespace KendoNET.DynamicLinq.Test +{ + [TestFixture] + public class AggregatorTestSystem + { + private MockContext _dbContext; + + + private static readonly JsonSerializerOptions jsonSerializerOptions = CustomJsonSerializerOptions.DefaultOptions; + + + public static IEnumerable DataSourceRequestWithAggregateSalarySum + { + get + { + + yield return JsonSerializer.Deserialize("{\"take\":10,\"skip\":0,\"aggregate\":[{\"field\":\"Salary\",\"aggregate\":\"sum\"}]}", jsonSerializerOptions); + + } + } + + public static IEnumerable DataSourceRequestWithManyAggregates + { + get + { + yield return JsonSerializer.Deserialize( + "{\"take\":10,\"skip\":0,\"aggregate\":[{\"field\":\"Salary\",\"aggregate\":\"sum\"},{\"field\":\"Salary\",\"aggregate\":\"average\"},{\"field\":\"Number\",\"aggregate\":\"max\"}]}", + jsonSerializerOptions); + + } + } + + [SetUp] + public void Setup() + { + _dbContext = MockContext.GetDefaultInMemoryDbContext(); + } + + [Test] + public void InputParameter_DecimalSum_CheckResultObjectString() + { + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(10, 0, null, null, + [ + new Aggregator + { + Aggregate = "sum", + Field = "Salary" + } + ], null); + + object expectedObject = "{ Salary = { sum = 14850 } }"; + ClassicAssert.AreEqual(expectedObject, result.Aggregates.ToString()); + } + + [TestCaseSource(nameof(DataSourceRequestWithAggregateSalarySum))] + public void InputDataSourceRequest_DecimalSum_CheckResultObjectString(DataSourceRequest dataSourceRequest) + { + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(dataSourceRequest); + + object expectedObject = "{ Salary = { sum = 14850 } }"; + ClassicAssert.AreEqual(expectedObject, result.Aggregates.ToString()); + } + + [TestCaseSource(nameof(DataSourceRequestWithAggregateSalarySum))] + public void InputDataSourceRequest_DecimalSum_CheckResultSum(DataSourceRequest dataSourceRequest) + { + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(dataSourceRequest); + var salaryAggregates = result.Aggregates.GetType().GetProperty("Salary")?.GetValue(result.Aggregates, null); + var salarySum = salaryAggregates?.GetType().GetProperty("sum")?.GetValue(salaryAggregates, null); + + const decimal expectedSalarySum = 14850; + ClassicAssert.AreEqual(expectedSalarySum, salarySum); + + const decimal incorrectSalarySum = 9999; + ClassicAssert.AreNotEqual(incorrectSalarySum, salarySum); + } + + [Test] + public void InputParameter_ManyAggregators_CheckResultObjectString() + { + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(10, 0, null, null, + [ + new Aggregator + { + Aggregate = "sum", + Field = "Salary" + }, + new Aggregator + { + Aggregate = "average", + Field = "Salary" + }, + new Aggregator + { + Aggregate = "max", + Field = "Number" + }, + ], null); + + object expectedObject = "{ Salary = { sum = 14850, average = 2970 }, Number = { max = 6 } }"; + ClassicAssert.AreEqual(expectedObject, result.Aggregates.ToString()); + } + + [TestCaseSource(nameof(DataSourceRequestWithManyAggregates))] + public void InputDataSourceRequest_ManyAggregators_CheckResultObjectString(DataSourceRequest dataSourceRequest) + { + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(dataSourceRequest); + + object expectedObject = "{ Salary = { sum = 14850, average = 2970 }, Number = { max = 6 } }"; + ClassicAssert.AreEqual(expectedObject, result.Aggregates.ToString()); + } + } +} \ No newline at end of file diff --git a/test/KendoNET.DynamicLinq.Test/CustomJsonSerializerOptions.cs b/test/KendoNET.DynamicLinq.Test/CustomJsonSerializerOptions.cs index 9fafabd..ef7053e 100644 --- a/test/KendoNET.DynamicLinq.Test/CustomJsonSerializerOptions.cs +++ b/test/KendoNET.DynamicLinq.Test/CustomJsonSerializerOptions.cs @@ -1,4 +1,4 @@ -#if NETCOREAPP3_1 + using System; using System.Text.Json; @@ -8,7 +8,7 @@ namespace KendoNET.DynamicLinq.Test { public class CustomJsonSerializerOptions { - public static readonly JsonSerializerOptions DefaultOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + public static readonly JsonSerializerOptions DefaultOptions = new() { PropertyNameCaseInsensitive = true }; static CustomJsonSerializerOptions() { @@ -35,7 +35,7 @@ public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS if (reader.TokenType == JsonTokenType.Number) { - if (reader.TryGetInt64(out long l)) + if (reader.TryGetInt64(out var l)) { return l; } @@ -45,7 +45,7 @@ public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS if (reader.TokenType == JsonTokenType.String) { - if (reader.TryGetDateTime(out DateTime datetime)) + if (reader.TryGetDateTime(out var datetime)) { return datetime; } @@ -53,7 +53,7 @@ public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS return reader.GetString(); } - using JsonDocument document = JsonDocument.ParseValue(ref reader); + using var document = JsonDocument.ParseValue(ref reader); return document.RootElement.Clone(); } @@ -61,5 +61,3 @@ public override void Write(Utf8JsonWriter writer, object objectToWrite, JsonSeri throw new InvalidOperationException("Should not get here."); } } - -#endif \ No newline at end of file diff --git a/test/KendoNET.DynamicLinq.Test/Data/MockContext.cs b/test/KendoNET.DynamicLinq.Test/Data/MockContext.cs index 3de60f3..8067da2 100644 --- a/test/KendoNET.DynamicLinq.Test/Data/MockContext.cs +++ b/test/KendoNET.DynamicLinq.Test/Data/MockContext.cs @@ -1,11 +1,11 @@ -using System; +using System; +using KendoNET.DynamicLinq.Test.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using KendoNET.DynamicLinq.Test.Models; namespace KendoNET.DynamicLinq.Test.Data { - public class MockContext : DbContext + public class MockContext(DbContextOptions options) : DbContext(options) { private static MockContext _defaultDbContext; @@ -24,10 +24,6 @@ public static MockContext GetDefaultInMemoryDbContext() return _defaultDbContext; } - public MockContext(DbContextOptions options) : base(options) - { - } - protected override void OnModelCreating(ModelBuilder modelBuilder) { // Add employee data diff --git a/test/KendoNET.DynamicLinq.Test/FilterTest.cs b/test/KendoNET.DynamicLinq.Test/FilterTest.cs index 6d50ff5..89a9125 100644 --- a/test/KendoNET.DynamicLinq.Test/FilterTest.cs +++ b/test/KendoNET.DynamicLinq.Test/FilterTest.cs @@ -1,15 +1,9 @@ -using System.Linq; -using Microsoft.EntityFrameworkCore; -using NUnit.Framework; +using System.Linq; using KendoNET.DynamicLinq.Test.Data; - -#if NETCOREAPP3_1 -using System.Text.Json; -#endif - -#if NETCOREAPP2_1 || NETCOREAPP2_2 +using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; -#endif +using NUnit.Framework; +using NUnit.Framework.Legacy; namespace KendoNET.DynamicLinq.Test { @@ -18,9 +12,7 @@ public class FilterTest { private MockContext _dbContext; -#if NETCOREAPP3_1 - private JsonSerializerOptions jsonSerializerOptions = CustomJsonSerializerOptions.DefaultOptions; -#endif + [SetUp] public void Setup() @@ -39,23 +31,23 @@ public void InputParameter_SubPropertyContains_CheckResultCount() Logic = "and" }); - Assert.AreEqual(2, result.Total); + ClassicAssert.AreEqual(2, result.Total); var result2 = _dbContext.Employee.AsQueryable().ToDataSourceResult(10, 0, null, new Filter { - Filters = new[] - { + Filters = + [ new Filter { Field = "Company.Name", Operator = "contains", Value = "Microsoft" } - }, + ], Logic = "and" }); - Assert.AreEqual(2, result2.Total); + ClassicAssert.AreEqual(2, result2.Total); } [Test] @@ -63,16 +55,12 @@ public void InputDataSourceRequest_DecimalGreaterAndLess_CheckResultCount() { // source string = {"take":20,"skip":0,"filter":{"logic":"and","filters":[{"field":"Salary","operator":"gt","value":999.00},{"field":"Salary","operator":"lt","value":6000.00}]}} -#if NETCOREAPP3_1 - var request = JsonSerializer.Deserialize( - "{\"take\":20,\"skip\":0,\"filter\":{\"logic\":\"and\",\"filters\":[{\"field\":\"Salary\",\"operator\":\"gt\",\"value\":999.00},{\"field\":\"Salary\",\"operator\":\"lt\",\"value\":6000.00}]}}", - jsonSerializerOptions); -#else + var request = JsonConvert.DeserializeObject( "{\"take\":20,\"skip\":0,\"filter\":{\"logic\":\"and\",\"filters\":[{\"field\":\"Salary\",\"operator\":\"gt\",\"value\":999.00},{\"field\":\"Salary\",\"operator\":\"lt\",\"value\":6000.00}]}}"); -#endif + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(request); - Assert.AreEqual(4, result.Total); + ClassicAssert.AreEqual(4, result.Total); } [Test] @@ -80,16 +68,12 @@ public void InputDataSourceRequest_DoubleGreaterAndLessEqual_CheckResultCount() { // source string = {"take":20,"skip":0,"filter":{"logic":"and","filters":[{"field":"Weight","operator":"gt","value":48},{"field":"Weight","operator":"lt","value":69.2}]}} -#if NETCOREAPP3_1 - var request = JsonSerializer.Deserialize( - "{\"take\":20,\"skip\":0,\"filter\":{\"logic\":\"and\",\"filters\":[{\"field\":\"Weight\",\"operator\":\"gt\",\"value\":48},{\"field\":\"Weight\",\"operator\":\"lte\",\"value\":69.2}]}}", - jsonSerializerOptions); -#else + var request = JsonConvert.DeserializeObject( "{\"take\":20,\"skip\":0,\"filter\":{\"logic\":\"and\",\"filters\":[{\"field\":\"Weight\",\"operator\":\"gt\",\"value\":48},{\"field\":\"Weight\",\"operator\":\"lte\",\"value\":69.2}]}}"); -#endif + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(request); - Assert.AreEqual(3, result.Total); + ClassicAssert.AreEqual(3, result.Total); } [Test] @@ -97,16 +81,12 @@ public void InputDataSourceRequest_ManyConditions_CheckResultCount() { // source string = {\"take\":10,\"skip\":0,\"filter\":{\"logic\":\"and\",\"filters\":[{\"logic\":\"or\",\"filters\":[{\"field\":\"Birthday\",\"operator\":\"eq\",\"value\":\"1986-10-09T16:00:00.000Z\"},{\"field\":\"Birthday\",\"operator\":\"eq\",\"value\":\"1976-11-05T16:00:00.000Z\"}]},{\"logic\":\"and\",\"filters\":[{\"field\":\"Salary\",\"operator\":\"gte\",\"value\":1000},{\"field\":\"Salary\",\"operator\":\"lte\",\"value\":6000}]}]}} -#if NETCOREAPP3_1 - var request = JsonSerializer.Deserialize( - "{\"take\":10,\"skip\":0,\"filter\":{\"logic\":\"and\",\"filters\":[{\"logic\":\"or\",\"filters\":[{\"field\":\"Birthday\",\"operator\":\"eq\",\"value\":\"1986-10-09T00:00:00.000Z\"},{\"field\":\"Birthday\",\"operator\":\"eq\",\"value\":\"1976-11-05T00:00:00.000Z\"}]},{\"logic\":\"and\",\"filters\":[{\"field\":\"Salary\",\"operator\":\"gte\",\"value\":1000},{\"field\":\"Salary\",\"operator\":\"lte\",\"value\":6000}]}]}}", - jsonSerializerOptions); -#else + var request = JsonConvert.DeserializeObject( "{\"take\":10,\"skip\":0,\"filter\":{\"logic\":\"and\",\"filters\":[{\"logic\":\"or\",\"filters\":[{\"field\":\"Birthday\",\"operator\":\"eq\",\"value\":\"1986-10-09T00:00:00.000Z\"},{\"field\":\"Birthday\",\"operator\":\"eq\",\"value\":\"1976-11-05T00:00:00.000Z\"}]},{\"logic\":\"and\",\"filters\":[{\"field\":\"Salary\",\"operator\":\"gte\",\"value\":1000},{\"field\":\"Salary\",\"operator\":\"lte\",\"value\":6000}]}]}}"); -#endif + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(request); - Assert.AreEqual(2, result.Total); + ClassicAssert.AreEqual(2, result.Total); } } } \ No newline at end of file diff --git a/test/KendoNET.DynamicLinq.Test/FilterTestSystem.cs b/test/KendoNET.DynamicLinq.Test/FilterTestSystem.cs new file mode 100644 index 0000000..f83267f --- /dev/null +++ b/test/KendoNET.DynamicLinq.Test/FilterTestSystem.cs @@ -0,0 +1,95 @@ +using System.Linq; +using System.Text.Json; +using KendoNET.DynamicLinq.Test.Data; +using Microsoft.EntityFrameworkCore; +using NUnit.Framework; +using NUnit.Framework.Legacy; + + +namespace KendoNET.DynamicLinq.Test +{ + [TestFixture] + public class FilterTestSystem + { + private MockContext _dbContext; + + private readonly JsonSerializerOptions jsonSerializerOptions = CustomJsonSerializerOptions.DefaultOptions; + + + [SetUp] + public void Setup() + { + _dbContext = MockContext.GetDefaultInMemoryDbContext(); + } + + [Test] + public void InputParameter_SubPropertyContains_CheckResultCount() + { + var result = _dbContext.Employee.Include(x => x.Company).AsQueryable().ToDataSourceResult(10, 0, null, new Filter + { + Field = "Company.Name", + Value = "Microsoft", + Operator = "contains", + Logic = "and" + }); + + ClassicAssert.AreEqual(2, result.Total); + + var result2 = _dbContext.Employee.AsQueryable().ToDataSourceResult(10, 0, null, new Filter + { + Filters = + [ + new Filter + { + Field = "Company.Name", + Operator = "contains", + Value = "Microsoft" + } + ], + Logic = "and" + }); + + ClassicAssert.AreEqual(2, result2.Total); + } + + [Test] + public void InputDataSourceRequest_DecimalGreaterAndLess_CheckResultCount() + { + // source string = {"take":20,"skip":0,"filter":{"logic":"and","filters":[{"field":"Salary","operator":"gt","value":999.00},{"field":"Salary","operator":"lt","value":6000.00}]}} + + + var request = JsonSerializer.Deserialize( + "{\"take\":20,\"skip\":0,\"filter\":{\"logic\":\"and\",\"filters\":[{\"field\":\"Salary\",\"operator\":\"gt\",\"value\":999.00},{\"field\":\"Salary\",\"operator\":\"lt\",\"value\":6000.00}]}}", + jsonSerializerOptions); + + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(request); + ClassicAssert.AreEqual(4, result.Total); + } + + [Test] + public void InputDataSourceRequest_DoubleGreaterAndLessEqual_CheckResultCount() + { + // source string = {"take":20,"skip":0,"filter":{"logic":"and","filters":[{"field":"Weight","operator":"gt","value":48},{"field":"Weight","operator":"lt","value":69.2}]}} + + var request = JsonSerializer.Deserialize( + "{\"take\":20,\"skip\":0,\"filter\":{\"logic\":\"and\",\"filters\":[{\"field\":\"Weight\",\"operator\":\"gt\",\"value\":48},{\"field\":\"Weight\",\"operator\":\"lte\",\"value\":69.2}]}}", + jsonSerializerOptions); + + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(request); + ClassicAssert.AreEqual(3, result.Total); + } + + [Test] + public void InputDataSourceRequest_ManyConditions_CheckResultCount() + { + // source string = {\"take\":10,\"skip\":0,\"filter\":{\"logic\":\"and\",\"filters\":[{\"logic\":\"or\",\"filters\":[{\"field\":\"Birthday\",\"operator\":\"eq\",\"value\":\"1986-10-09T16:00:00.000Z\"},{\"field\":\"Birthday\",\"operator\":\"eq\",\"value\":\"1976-11-05T16:00:00.000Z\"}]},{\"logic\":\"and\",\"filters\":[{\"field\":\"Salary\",\"operator\":\"gte\",\"value\":1000},{\"field\":\"Salary\",\"operator\":\"lte\",\"value\":6000}]}]}} + + var request = JsonSerializer.Deserialize( + "{\"take\":10,\"skip\":0,\"filter\":{\"logic\":\"and\",\"filters\":[{\"logic\":\"or\",\"filters\":[{\"field\":\"Birthday\",\"operator\":\"eq\",\"value\":\"1986-10-09T00:00:00.000Z\"},{\"field\":\"Birthday\",\"operator\":\"eq\",\"value\":\"1976-11-05T00:00:00.000Z\"}]},{\"logic\":\"and\",\"filters\":[{\"field\":\"Salary\",\"operator\":\"gte\",\"value\":1000},{\"field\":\"Salary\",\"operator\":\"lte\",\"value\":6000}]}]}}", + jsonSerializerOptions); + + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(request); + ClassicAssert.AreEqual(2, result.Total); + } + } +} \ No newline at end of file diff --git a/test/KendoNET.DynamicLinq.Test/GroupTest.cs b/test/KendoNET.DynamicLinq.Test/GroupTest.cs index 4d70fda..a6a3fc8 100644 --- a/test/KendoNET.DynamicLinq.Test/GroupTest.cs +++ b/test/KendoNET.DynamicLinq.Test/GroupTest.cs @@ -1,14 +1,9 @@ -using NUnit.Framework; -using System.Linq.Dynamic.Core; +using System.Linq.Dynamic.Core; using KendoNET.DynamicLinq.Test.Data; - -#if NETCOREAPP3_1 -using System.Text.Json; -#endif - -#if NETCOREAPP2_1 || NETCOREAPP2_2 using Newtonsoft.Json; -#endif +using NUnit.Framework; +using NUnit.Framework.Legacy; + namespace KendoNET.DynamicLinq.Test { @@ -17,9 +12,7 @@ public class GroupTest { private MockContext _dbContext; -#if NETCOREAPP3_1 - private JsonSerializerOptions _jsonSerializerOptions = CustomJsonSerializerOptions.DefaultOptions; -#endif + [SetUp] public void Setup() @@ -32,16 +25,13 @@ public void DataSourceRequest_EnumField_GroupedCount() { // source string = {"take":20,"skip":0,"sort":[{"field":"Number","dir":"desc"}],"group":[{"field":"Gender"}]} -#if NETCOREAPP3_1 - var request = JsonSerializer.Deserialize("{\"take\":20,\"skip\":0,\"sort\":[{\"field\":\"Number\",\"dir\":\"desc\"}],\"group\":[{\"field\":\"Gender\"}]}", - _jsonSerializerOptions); -#else + var request = JsonConvert.DeserializeObject("{\"take\":20,\"skip\":0,\"sort\":[{\"field\":\"Number\",\"dir\":\"desc\"}],\"group\":[{\"field\":\"Gender\"}]}"); -#endif + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(request); var groupItems = result.Groups.ToDynamicList().Count; - Assert.AreEqual(3, groupItems); + ClassicAssert.AreEqual(3, groupItems); } } } \ No newline at end of file diff --git a/test/KendoNET.DynamicLinq.Test/GroupTestSystem.cs b/test/KendoNET.DynamicLinq.Test/GroupTestSystem.cs new file mode 100644 index 0000000..8d304f4 --- /dev/null +++ b/test/KendoNET.DynamicLinq.Test/GroupTestSystem.cs @@ -0,0 +1,37 @@ +using System.Linq.Dynamic.Core; +using System.Text.Json; +using KendoNET.DynamicLinq.Test.Data; +using NUnit.Framework; +using NUnit.Framework.Legacy; + +namespace KendoNET.DynamicLinq.Test +{ + [TestFixture] + public class GroupTestSystem + { + private MockContext _dbContext; + + + private readonly JsonSerializerOptions _jsonSerializerOptions = CustomJsonSerializerOptions.DefaultOptions; + + + [SetUp] + public void Setup() + { + _dbContext = MockContext.GetDefaultInMemoryDbContext(); + } + + [Test] + public void DataSourceRequest_EnumField_GroupedCount() + { + // source string = {"take":20,"skip":0,"sort":[{"field":"Number","dir":"desc"}],"group":[{"field":"Gender"}]} + + var request = JsonSerializer.Deserialize("{\"take\":20,\"skip\":0,\"sort\":[{\"field\":\"Number\",\"dir\":\"desc\"}],\"group\":[{\"field\":\"Gender\"}]}", + _jsonSerializerOptions); + + var result = _dbContext.Employee.AsQueryable().ToDataSourceResult(request); + var groupItems = result.Groups.ToDynamicList().Count; + ClassicAssert.AreEqual(3, groupItems); + } + } +} \ No newline at end of file diff --git a/test/KendoNET.DynamicLinq.Test/KendoNET.DynamicLinq.Test.csproj b/test/KendoNET.DynamicLinq.Test/KendoNET.DynamicLinq.Test.csproj index 032a16f..b26dfd0 100644 --- a/test/KendoNET.DynamicLinq.Test/KendoNET.DynamicLinq.Test.csproj +++ b/test/KendoNET.DynamicLinq.Test/KendoNET.DynamicLinq.Test.csproj @@ -1,21 +1,21 @@ - + - netcoreapp2.1;netcoreapp3.1 + net8.0 false KendoNET.DynamicLinq.Test - - - - - + + + + + - +