diff --git a/README.md b/README.md index 1787cc5e..356dbe26 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,134 @@ +********************************************************************************************************************************************** + +This is a fork of Devexpress.AspNet.Data, a fantastic expression building library for querying an API. While designed to work with Devex controls, it also works as the base for free querying of Entity Framework Core and XPO data models. + +Its main shortcoming in my use case was a lack of support for Automapper style projections. This fork provides those projections and also changes to support the injection of automapper parameters for use in customfilters. + +Available as nuget at https://www.nuget.org/packages/DXAutomap.AspNet.Data/1.12.0 + +NuGet\Install-Package DXAutomap.AspNet.Data -Version 1.12.0 + +********************************************************************************************************************************************** + +How to use: + +The basic functionality is exactly the same as the devex library. The big difference is that the user can: +1. Specify a projection type to the LoadAsync method (ProjectTo in automapper). As long as this type is included in the registered mapping config(s), the projection will occur seamlessly with the results in the returned object. While it is possible to specify an (already projected) IQueryable to the LoadAsync in the base library, you lose the capacity to filter, sort or group on properties of the base object that aren't in the projection. **This library supports querying any property of the base object OR the mapped object** +2. Sort and group on original or mapped properties +3. Specify parameters to the LoadAsync method that will be accessible in the custom filters added through CustomFilterCompilers. This is important when you are passing parameters to an automapper ProjectTo that would change filters applied to the base object. +4. Specify that filters apply after a projection using the new DataSourceLoadOption property ProjectBeforeFilter +5. Utilise the devex expression builders for binary expressions by calling CompileNonCustomBinary +6. From Version 1.11, CustomAccessors also have access to the tuntime context (Automapper object) + + +********************************************************************************************************************************************** + +Quick Start: +1. Register your mappings: + +In startup register automapper as per usual + + public void ConfigureServices(IServiceCollection services) + { + .... + List lstAssembly = new List() { typeof(AutoMapperProfileService).GetTypeInfo().Assembly, typeof(AutoMapperProfileORM).GetTypeInfo().Assembly }; + services.AddAutoMapper(lstAssembly); + .... + } + +In configure, call CustomAccessorCompilers.RegisterAutomapperProfiles. You can also add other accessors for filtering and sorting if you like. + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + .... + + CustomFilters.RegisterFilters(); // + CustomAccessorCompilers.RegisterAutomapperProfiles(app.ApplicationServices.GetService()); + //Additional mappers can be added like this: + //CustomAccessorLibrary.Add("FirstLetter", t => t.AreaCodeName.FirstOrDefault().ToString().ToUpper()); + //Or like this - a separate static class/method is recommended + //CustomAccessorCompilers.RegisterContext("DateRead", (u) => + //{ + // dynamic dynObj = new DynamicWrapper(u); + // if (dynObj.UserId is int) + // { + // int UserId = dynObj.UserId; + // return src => src.NotificationTos.Where(x => x.UserId == UserId).OrderByDescending(x => x.NotificationTypeId).Select(x => x.DateRead).FirstOrDefault(); + // } + // return src => null; + //}); + .... + } + +2. If you have any custom filters requiring additional context (such as an object that is used in the Projection - refer below), you can register your filters using the RegisterBinaryExpressionCompilerWithContext. This works like devexpresses RegisterBinaryExpressionCompiler method, but includes the context. RegisterBinaryExpressionCompiler will still work for simpler maps. The example uses linqKit + + + + public static partial class CustomFilters + { + public static void RegisterFilters() + { + CustomFilterCompilers.RegisterBinaryExpressionCompilerWithContext((info, rtContext) => + { + if (info.DataItemExpression.Type == typeof(Notification)) + { + if (info.AccessorText == "DateDismissed") + { + dynamic dynObj = new DynamicWrapper(rtContext.RuntimeResolutionContext); + if (int.TryParse(dynObj.UserId.ToString(), out int _ui)) + { + var pBaseExp = info.DataItemExpression as ParameterExpression; + var pBaseProperty = Expression.PropertyOrField(pBaseExp, "NotificationTos"); + + var _p = Expression.Parameter(typeof(NotificationTo), "nTo"); + var baseResult = rtContext.CompileNonCustomBinary(_p, GetParametersAsList(info)); + var predicate = PredicateBuilder.New(Expression.Lambda>(baseResult, _p)); + predicate.And((p) => p.UserId == _ui); + + var result = Expression.Call( + typeof(Enumerable), "Any", new[] { _p.Type }, + pBaseProperty, predicate); + + var ex22 = Expression.Lambda(result, pBaseExp) as Expression>; + return CompileWhereExpression(info, ex22).Body; + } + } + } + return null; + }); + } + } + + static IList GetParametersAsList(IBinaryExpressionInfo info) + { + var criteria = new List() { info.AccessorText }; + if (!string.IsNullOrWhiteSpace(info.Operation)) criteria.Add(info.Operation); + criteria.Add(info.Value); + return criteria; + } + +3. Call your projection from LoadAsyncDto (or LoadDto) + + return await DataSourceLoader.LoadAsync(source, options, mapper, MapParameters); + + mapper is your IMapper for automap + MapParameters are an object (usually null) that can be used for injecting context into a Projection. It is the second parameter of Automappers ProjectTo. + + + +********************************************************************************************************************************************** + +How it works: +- Beyond the mundane, there are three principle things here: +-- RegisterAutomapperProfiles iterates through every automapper map and creates a library of custom expressions that is resolved by a CompilerFunc registered using the base library's CustomAccessorCompilers.Register method. When expressions for sorting, filtering etc are constructed for a type, this library is queried to assemble any properties existing in the automap. +-- LoadAsync fetches any map between T and TDto and constructs a select expression that is then injected into the devexpress expression resolution for select as if it was passed as an option. The output object is typed to the TDto. +-- The CompileNonCustomBinary is available in CustomFilters registered using RegisterBinaryExpressionCompilerWithContext. This means the devexpress Expression compiler for all operators such as "=", "<>" "<" ">" "contains" etc can be resolved without explicitly doing it yourself. This method is in the instance of FilterExpressionCompiler passed into the custom filter (second parameter) + +********************************************************************************************************************************************** + + + + # DevExtreme ASP.NET Data [![CI](https://github.com/DevExpress/DevExtreme.AspNet.Data/actions/workflows/ci.yml/badge.svg?branch=master&event=push)](https://github.com/DevExpress/DevExtreme.AspNet.Data/actions/workflows/ci.yml) diff --git a/net/.cr/personal/FavoritesList/List.xml b/net/.cr/personal/FavoritesList/List.xml new file mode 100644 index 00000000..a60e5ed6 --- /dev/null +++ b/net/.cr/personal/FavoritesList/List.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/net/DevExtreme.AspNet.Data.Tests.Common/DevExtreme.AspNet.Data.Tests.Common.csproj b/net/DevExtreme.AspNet.Data.Tests.Common/DevExtreme.AspNet.Data.Tests.Common.csproj index 7a31e610..a9df0603 100644 --- a/net/DevExtreme.AspNet.Data.Tests.Common/DevExtreme.AspNet.Data.Tests.Common.csproj +++ b/net/DevExtreme.AspNet.Data.Tests.Common/DevExtreme.AspNet.Data.Tests.Common.csproj @@ -1,7 +1,7 @@ - net461;net6.0 + net7.0 DevExtreme.AspNet.Data.Tests False diff --git a/net/DevExtreme.AspNet.Data.Tests.Common/SampleLoadOptions.cs b/net/DevExtreme.AspNet.Data.Tests.Common/SampleLoadOptions.cs index 2f43b785..273e6353 100644 --- a/net/DevExtreme.AspNet.Data.Tests.Common/SampleLoadOptions.cs +++ b/net/DevExtreme.AspNet.Data.Tests.Common/SampleLoadOptions.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System.Collections.Generic; namespace DevExtreme.AspNet.Data.Tests { diff --git a/net/DevExtreme.AspNet.Data.Tests.EFCore7/DevExtreme.AspNet.Data.Tests.EFCore7.csproj b/net/DevExtreme.AspNet.Data.Tests.EFCore7/DevExtreme.AspNet.Data.Tests.EFCore7.csproj index 535db241..fd0a7a8c 100644 --- a/net/DevExtreme.AspNet.Data.Tests.EFCore7/DevExtreme.AspNet.Data.Tests.EFCore7.csproj +++ b/net/DevExtreme.AspNet.Data.Tests.EFCore7/DevExtreme.AspNet.Data.Tests.EFCore7.csproj @@ -1,4 +1,4 @@ - + net7.0 @@ -15,7 +15,6 @@ - diff --git a/net/DevExtreme.AspNet.Data.Tests.Xpo/DevExtreme.AspNet.Data.Tests.Xpo.csproj b/net/DevExtreme.AspNet.Data.Tests.Xpo/DevExtreme.AspNet.Data.Tests.Xpo.csproj index c273d591..214b9c9a 100644 --- a/net/DevExtreme.AspNet.Data.Tests.Xpo/DevExtreme.AspNet.Data.Tests.Xpo.csproj +++ b/net/DevExtreme.AspNet.Data.Tests.Xpo/DevExtreme.AspNet.Data.Tests.Xpo.csproj @@ -5,7 +5,7 @@ - + diff --git a/net/DevExtreme.AspNet.Data.Tests/DevExtreme.AspNet.Data.Tests.csproj b/net/DevExtreme.AspNet.Data.Tests/DevExtreme.AspNet.Data.Tests.csproj index df27a61b..e9ade0e9 100644 --- a/net/DevExtreme.AspNet.Data.Tests/DevExtreme.AspNet.Data.Tests.csproj +++ b/net/DevExtreme.AspNet.Data.Tests/DevExtreme.AspNet.Data.Tests.csproj @@ -1,7 +1,7 @@  - net6.0 + net7.0 NEWTONSOFT_TESTS diff --git a/net/DevExtreme.AspNet.Data.sln b/net/DevExtreme.AspNet.Data.sln index e35b0f1f..41c931f6 100644 --- a/net/DevExtreme.AspNet.Data.sln +++ b/net/DevExtreme.AspNet.Data.sln @@ -12,34 +12,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevExtreme.AspNet.Data", "DevExtreme.AspNet.Data\DevExtreme.AspNet.Data.csproj", "{F91204F2-1F3A-433F-871D-5F31B769FA90}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevExtreme.AspNet.Data.Tests", "DevExtreme.AspNet.Data.Tests\DevExtreme.AspNet.Data.Tests.csproj", "{BFF5CDCB-B8E0-4D4C-BCEB-18651691E1E1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample", "Sample\Sample.csproj", "{760B3927-BB40-4D5B-B64C-880E75E4A4A5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevExtreme.AspNet.Data.Tests.NET4", "DevExtreme.AspNet.Data.Tests.NET4\DevExtreme.AspNet.Data.Tests.NET4.csproj", "{D083C7D4-E8EC-409F-8B45-512FABB21A54}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevExtreme.AspNet.Data.Tests.EF6", "DevExtreme.AspNet.Data.Tests.EF6\DevExtreme.AspNet.Data.Tests.EF6.csproj", "{C401BB6E-D21B-45D5-AB98-6B308DB4E4FB}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevExtreme.AspNet.Data.Tests.Common", "DevExtreme.AspNet.Data.Tests.Common\DevExtreme.AspNet.Data.Tests.Common.csproj", "{F5878AD6-459F-4FCE-9BFB-54EE94D2083F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevExtreme.AspNet.Data.Tests.Xpo", "DevExtreme.AspNet.Data.Tests.Xpo\DevExtreme.AspNet.Data.Tests.Xpo.csproj", "{074B5988-B409-4BEE-BF12-79762B459857}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevExtreme.AspNet.Data.Tests.NH", "DevExtreme.AspNet.Data.Tests.NH\DevExtreme.AspNet.Data.Tests.NH.csproj", "{B9B11471-3317-4A42-B3AE-1879B76E3941}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevExtreme.AspNet.Data.Tests.L2S", "DevExtreme.AspNet.Data.Tests.L2S\DevExtreme.AspNet.Data.Tests.L2S.csproj", "{190E01CD-78FC-4578-ACBF-7D6C4DC57C18}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevExtreme.AspNet.Data.Tests.EFCore3", "DevExtreme.AspNet.Data.Tests.EFCore3\DevExtreme.AspNet.Data.Tests.EFCore3.csproj", "{3DAF3316-6B1B-4961-831A-790647662554}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevExtreme.AspNet.Data.Tests.EFCore5", "DevExtreme.AspNet.Data.Tests.EFCore5\DevExtreme.AspNet.Data.Tests.EFCore5.csproj", "{B8063440-8E4C-4D74-AF92-AE1737FAFD6F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevExtreme.AspNet.Data.Tests.EFCore6", "DevExtreme.AspNet.Data.Tests.EFCore6\DevExtreme.AspNet.Data.Tests.EFCore6.csproj", "{82748269-8B2F-4D65-8367-3D75146335F2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevExtreme.AspNet.Data.Tests.EFCore7", "DevExtreme.AspNet.Data.Tests.EFCore7\DevExtreme.AspNet.Data.Tests.EFCore7.csproj", "{EE44C4FD-0448-4F09-9612-6040E1DD2A8F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevExtreme.AspNet.Data.Tests.EFCore8", "DevExtreme.AspNet.Data.Tests.EFCore8\DevExtreme.AspNet.Data.Tests.EFCore8.csproj", "{CD8E0248-F0E8-4CE4-94C0-F8905E37D97F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevExtreme.AspNet.Data.Tests.EFCore9", "DevExtreme.AspNet.Data.Tests.EFCore9\DevExtreme.AspNet.Data.Tests.EFCore9.csproj", "{613530E9-013E-4FBD-BC3C-17F28B0C78EA}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -50,64 +22,11 @@ Global {F91204F2-1F3A-433F-871D-5F31B769FA90}.Debug|Any CPU.Build.0 = Debug|Any CPU {F91204F2-1F3A-433F-871D-5F31B769FA90}.Release|Any CPU.ActiveCfg = Release|Any CPU {F91204F2-1F3A-433F-871D-5F31B769FA90}.Release|Any CPU.Build.0 = Release|Any CPU - {BFF5CDCB-B8E0-4D4C-BCEB-18651691E1E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BFF5CDCB-B8E0-4D4C-BCEB-18651691E1E1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BFF5CDCB-B8E0-4D4C-BCEB-18651691E1E1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BFF5CDCB-B8E0-4D4C-BCEB-18651691E1E1}.Release|Any CPU.Build.0 = Release|Any CPU - {760B3927-BB40-4D5B-B64C-880E75E4A4A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {760B3927-BB40-4D5B-B64C-880E75E4A4A5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {760B3927-BB40-4D5B-B64C-880E75E4A4A5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {760B3927-BB40-4D5B-B64C-880E75E4A4A5}.Release|Any CPU.Build.0 = Release|Any CPU - {D083C7D4-E8EC-409F-8B45-512FABB21A54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D083C7D4-E8EC-409F-8B45-512FABB21A54}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D083C7D4-E8EC-409F-8B45-512FABB21A54}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D083C7D4-E8EC-409F-8B45-512FABB21A54}.Release|Any CPU.Build.0 = Release|Any CPU - {C401BB6E-D21B-45D5-AB98-6B308DB4E4FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C401BB6E-D21B-45D5-AB98-6B308DB4E4FB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C401BB6E-D21B-45D5-AB98-6B308DB4E4FB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C401BB6E-D21B-45D5-AB98-6B308DB4E4FB}.Release|Any CPU.Build.0 = Release|Any CPU - {F5878AD6-459F-4FCE-9BFB-54EE94D2083F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F5878AD6-459F-4FCE-9BFB-54EE94D2083F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F5878AD6-459F-4FCE-9BFB-54EE94D2083F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F5878AD6-459F-4FCE-9BFB-54EE94D2083F}.Release|Any CPU.Build.0 = Release|Any CPU - {074B5988-B409-4BEE-BF12-79762B459857}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {074B5988-B409-4BEE-BF12-79762B459857}.Debug|Any CPU.Build.0 = Debug|Any CPU - {074B5988-B409-4BEE-BF12-79762B459857}.Release|Any CPU.ActiveCfg = Release|Any CPU - {074B5988-B409-4BEE-BF12-79762B459857}.Release|Any CPU.Build.0 = Release|Any CPU - {B9B11471-3317-4A42-B3AE-1879B76E3941}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B9B11471-3317-4A42-B3AE-1879B76E3941}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B9B11471-3317-4A42-B3AE-1879B76E3941}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B9B11471-3317-4A42-B3AE-1879B76E3941}.Release|Any CPU.Build.0 = Release|Any CPU - {190E01CD-78FC-4578-ACBF-7D6C4DC57C18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {190E01CD-78FC-4578-ACBF-7D6C4DC57C18}.Debug|Any CPU.Build.0 = Debug|Any CPU - {190E01CD-78FC-4578-ACBF-7D6C4DC57C18}.Release|Any CPU.ActiveCfg = Release|Any CPU - {190E01CD-78FC-4578-ACBF-7D6C4DC57C18}.Release|Any CPU.Build.0 = Release|Any CPU - {3DAF3316-6B1B-4961-831A-790647662554}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3DAF3316-6B1B-4961-831A-790647662554}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3DAF3316-6B1B-4961-831A-790647662554}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3DAF3316-6B1B-4961-831A-790647662554}.Release|Any CPU.Build.0 = Release|Any CPU - {B8063440-8E4C-4D74-AF92-AE1737FAFD6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B8063440-8E4C-4D74-AF92-AE1737FAFD6F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B8063440-8E4C-4D74-AF92-AE1737FAFD6F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B8063440-8E4C-4D74-AF92-AE1737FAFD6F}.Release|Any CPU.Build.0 = Release|Any CPU - {82748269-8B2F-4D65-8367-3D75146335F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {82748269-8B2F-4D65-8367-3D75146335F2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {82748269-8B2F-4D65-8367-3D75146335F2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {82748269-8B2F-4D65-8367-3D75146335F2}.Release|Any CPU.Build.0 = Release|Any CPU - {EE44C4FD-0448-4F09-9612-6040E1DD2A8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EE44C4FD-0448-4F09-9612-6040E1DD2A8F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EE44C4FD-0448-4F09-9612-6040E1DD2A8F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EE44C4FD-0448-4F09-9612-6040E1DD2A8F}.Release|Any CPU.Build.0 = Release|Any CPU - {CD8E0248-F0E8-4CE4-94C0-F8905E37D97F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CD8E0248-F0E8-4CE4-94C0-F8905E37D97F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CD8E0248-F0E8-4CE4-94C0-F8905E37D97F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CD8E0248-F0E8-4CE4-94C0-F8905E37D97F}.Release|Any CPU.Build.0 = Release|Any CPU - {613530E9-013E-4FBD-BC3C-17F28B0C78EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {613530E9-013E-4FBD-BC3C-17F28B0C78EA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {613530E9-013E-4FBD-BC3C-17F28B0C78EA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {613530E9-013E-4FBD-BC3C-17F28B0C78EA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {FDC8FE66-8841-4614-978F-0581BA981284} + EndGlobalSection EndGlobal diff --git a/net/DevExtreme.AspNet.Data/Aggregation/AggregateCalculator.cs b/net/DevExtreme.AspNet.Data/Aggregation/AggregateCalculator.cs index d37264f3..7f969783 100644 --- a/net/DevExtreme.AspNet.Data/Aggregation/AggregateCalculator.cs +++ b/net/DevExtreme.AspNet.Data/Aggregation/AggregateCalculator.cs @@ -20,10 +20,10 @@ class AggregateCalculator { Stack[]> _groupAggregatorsStack; - public AggregateCalculator(IEnumerable data, IAccessor accessor, IReadOnlyList totalSummary, IReadOnlyList groupSummary, SumFix sumFix = null) { + public AggregateCalculator(IEnumerable data, IAccessor accessor, IReadOnlyList totalSummary, IReadOnlyList groupSummary, SumFix sumFix = null, object runtimeResolutionContext = null) { _data = data; _accessor = accessor; - _sumFix = sumFix ?? new SumFix(typeof(T), totalSummary, groupSummary); + _sumFix = sumFix ?? new SumFix(typeof(T), totalSummary, groupSummary, runtimeResolutionContext); if(totalSummary != null && totalSummary.Count > 0) _totalAggregators = totalSummary.Select(CreateAggregator).ToArray(); diff --git a/net/DevExtreme.AspNet.Data/Aggregation/SumFix.cs b/net/DevExtreme.AspNet.Data/Aggregation/SumFix.cs index 7d11c0f2..0a87f438 100644 --- a/net/DevExtreme.AspNet.Data/Aggregation/SumFix.cs +++ b/net/DevExtreme.AspNet.Data/Aggregation/SumFix.cs @@ -17,8 +17,8 @@ class SumFix : ExpressionCompiler { IReadOnlyList _groupSummary; IDictionary _defaultValues; - public SumFix(Type itemType, IReadOnlyList totalSummary, IReadOnlyList groupSummary) - : base(itemType, false) { + public SumFix(Type itemType, IReadOnlyList totalSummary, IReadOnlyList groupSummary, object runtimeResolutionContext) + : base(itemType, false, runtimeResolutionContext) { _totalSummary = totalSummary; _groupSummary = groupSummary; } diff --git a/net/DevExtreme.AspNet.Data/AssemblyInfo.cs b/net/DevExtreme.AspNet.Data/AssemblyInfo.cs index 9f464be9..6447e483 100644 --- a/net/DevExtreme.AspNet.Data/AssemblyInfo.cs +++ b/net/DevExtreme.AspNet.Data/AssemblyInfo.cs @@ -8,8 +8,3 @@ [assembly: InternalsVisibleTo("DevExtreme.AspNet.Data.Tests.Xpo")] #endif -[assembly: CLSCompliant(true)] - -#if !DEBUG -[assembly: AssemblyKeyFile("release.snk")] -#endif diff --git a/net/DevExtreme.AspNet.Data/DataSourceExpressionBuilder.cs b/net/DevExtreme.AspNet.Data/DataSourceExpressionBuilder.cs index a21a003d..c19a752e 100644 --- a/net/DevExtreme.AspNet.Data/DataSourceExpressionBuilder.cs +++ b/net/DevExtreme.AspNet.Data/DataSourceExpressionBuilder.cs @@ -1,4 +1,5 @@ -using DevExtreme.AspNet.Data.RemoteGrouping; +using AutoMapper.Internal; +using DevExtreme.AspNet.Data.RemoteGrouping; using System; using System.Collections; using System.Collections.Generic; @@ -16,13 +17,22 @@ public DataSourceExpressionBuilder(Expression expr, DataSourceLoadContext contex Context = context; } - public Expression BuildLoadExpr(bool paginate, IList filterOverride = null, IReadOnlyList selectOverride = null) { - AddFilter(filterOverride); - AddSort(); - AddSelect(selectOverride); - if(paginate) - AddPaging(); - return Expr; + public Expression BuildLoadExpr(bool paginate, IList filterOverride = null, IReadOnlyList selectOverride = null, Type projectionType = null) { + if(Context.ProjectBeforeFilter) { + AddSelect(selectOverride, projectionType); + AddFilter(filterOverride); + AddSort(); + if(paginate) + AddPaging(); + return Expr; + } else { + AddFilter(filterOverride); + AddSort(); + AddSelect(selectOverride, projectionType); + if(paginate) + AddPaging(); + return Expr; + } } public Expression BuildCountExpr() { @@ -31,7 +41,10 @@ public Expression BuildCountExpr() { return Expr; } - public Expression BuildLoadGroupsExpr(bool paginate, bool suppressGroups = false, bool suppressTotals = false) { + public Expression BuildLoadGroupsExpr(bool paginate, bool suppressGroups = false, bool suppressTotals = false, Type projectionType = null) { + if(Context.ProjectBeforeFilter) { + AddSelect(null, projectionType); + } AddFilter(); AddRemoteGrouping(suppressGroups, suppressTotals); if(paginate) @@ -51,7 +64,7 @@ void AddFilter(IList filterOverride = null) { if(filterOverride != null || Context.HasFilter) { var filterExpr = filterOverride != null && filterOverride.Count < 1 ? Expression.Lambda(Expression.Constant(false), Expression.Parameter(GetItemType())) - : new FilterExpressionCompiler(GetItemType(), Context.GuardNulls, Context.UseStringToLower, Context.SupportsEqualsMethod).Compile(filterOverride ?? Context.Filter); + : new FilterExpressionCompiler(GetItemType(), Context.GuardNulls, Context.UseStringToLower, Context.SupportsEqualsMethod, Context.AutomapperProjectionParameters).Compile(filterOverride ?? Context.Filter); Expr = QueryableCall(nameof(Queryable.Where), Expression.Quote(filterExpr)); } @@ -59,12 +72,25 @@ void AddFilter(IList filterOverride = null) { void AddSort() { if(Context.HasAnySort) - Expr = new SortExpressionCompiler(GetItemType(), Context.GuardNulls).Compile(Expr, Context.GetFullSort()); + Expr = new SortExpressionCompiler(GetItemType(), Context.GuardNulls, Context.AutomapperProjectionParameters).Compile(Expr, Context.GetFullSort()); } - void AddSelect(IReadOnlyList selectOverride = null) { + void AddSelect(IReadOnlyList selectOverride = null, Type projectionType = null) { if(selectOverride != null || Context.HasAnySelect && Context.UseRemoteSelect) Expr = CreateSelectCompiler().Compile(Expr, selectOverride ?? Context.FullSelect); + else if(projectionType != null) { + var _type = GetItemType(); + var qryBase = Context.Mapper.ConfigurationProvider.Internal().ProjectionBuilder.GetProjection(_type, projectionType, Context.AutomapperProjectionParameters, Array.Empty()); + var qryExpr = qryBase.Projection; + if(qryBase.LetClause != null && qryBase.Projection != null) { + //Automapper can multi-stage complex queries. Need to push one select into another. + var srcType = qryExpr.Parameters[0].Type; + Expr = Expression.Call(typeof(Queryable), nameof(Queryable.Select), new[] { _type, qryBase.LetClause.ReturnType }, Expr, qryBase.LetClause); + Expr = Expression.Call(typeof(Queryable), nameof(Queryable.Select), new[] { qryBase.LetClause.ReturnType, qryBase.Projection.ReturnType }, Expr, qryBase.Projection); + } else { + Expr = Expression.Call(typeof(Queryable), nameof(Queryable.Select), new[] { _type, qryExpr.ReturnType }, Expr, Expression.Quote(qryExpr)); + } + } } void AddPaging() { @@ -80,7 +106,8 @@ void AddRemoteGrouping(bool suppressGroups, bool suppressTotals) { GetItemType(), Context.GuardNulls, Context.ExpandLinqSumType, Context.CreateAnonTypeNewTweaks(), suppressGroups ? null : Context.Group, suppressTotals ? null : Context.TotalSummary, - suppressGroups ? null : Context.GroupSummary + suppressGroups ? null : Context.GroupSummary, + Context.AutomapperProjectionParameters ); Expr = compiler.Compile(Expr); } @@ -90,7 +117,7 @@ void AddCount() { } SelectExpressionCompiler CreateSelectCompiler() - => new SelectExpressionCompiler(GetItemType(), Context.GuardNulls, Context.CreateAnonTypeNewTweaks()); + => new SelectExpressionCompiler(GetItemType(), Context.GuardNulls, Context.CreateAnonTypeNewTweaks(), Context.AutomapperProjectionParameters); Expression QueryableCall(string methodName) => Expression.Call(typeof(Queryable), methodName, GetQueryableGenericArguments(), Expr); diff --git a/net/DevExtreme.AspNet.Data/DataSourceLoadContext.cs b/net/DevExtreme.AspNet.Data/DataSourceLoadContext.cs index aa27ea7c..1b1c5985 100644 --- a/net/DevExtreme.AspNet.Data/DataSourceLoadContext.cs +++ b/net/DevExtreme.AspNet.Data/DataSourceLoadContext.cs @@ -1,4 +1,5 @@ -using DevExtreme.AspNet.Data.Aggregation; +using AutoMapper; +using DevExtreme.AspNet.Data.Aggregation; using DevExtreme.AspNet.Data.Helpers; using DevExtreme.AspNet.Data.Types; using System; @@ -12,11 +13,14 @@ partial class DataSourceLoadContext { readonly DataSourceLoadOptionsBase _options; readonly QueryProviderInfo _providerInfo; readonly Type _itemType; + readonly IMapper _mapper; - public DataSourceLoadContext(DataSourceLoadOptionsBase options, QueryProviderInfo providerInfo, Type itemType) { + public DataSourceLoadContext(DataSourceLoadOptionsBase options, QueryProviderInfo providerInfo, Type itemType, + IMapper mapper = null) { _options = options; _providerInfo = providerInfo; _itemType = itemType; + _mapper = mapper; } public bool GuardNulls { @@ -79,6 +83,15 @@ partial class DataSourceLoadContext { public bool SupportsEqualsMethod => !_providerInfo.IsXPO; } + // Projection + partial class DataSourceLoadContext { + public IMapper Mapper => _mapper; + public bool HasProjection => _mapper != null; + public object AutomapperProjectionParameters { get; set; } + public bool ProjectBeforeFilter => _options.ProjectBeforeFilter ?? false; + } + + // Grouping partial class DataSourceLoadContext { bool? @@ -307,4 +320,6 @@ string[] Init() { } } } + + } diff --git a/net/DevExtreme.AspNet.Data/DataSourceLoadOptionsBase.cs b/net/DevExtreme.AspNet.Data/DataSourceLoadOptionsBase.cs index 8275faa6..b7b6b692 100644 --- a/net/DevExtreme.AspNet.Data/DataSourceLoadOptionsBase.cs +++ b/net/DevExtreme.AspNet.Data/DataSourceLoadOptionsBase.cs @@ -118,6 +118,7 @@ public class DataSourceLoadOptionsBase { public bool? SortByPrimaryKey { get; set; } public bool AllowAsyncOverSync { get; set; } + public bool? ProjectBeforeFilter { get; set; } #if DEBUG internal Action ExpressionWatcher; diff --git a/net/DevExtreme.AspNet.Data/DataSourceLoader.cs b/net/DevExtreme.AspNet.Data/DataSourceLoader.cs index ea4f6a1a..3ecea1de 100644 --- a/net/DevExtreme.AspNet.Data/DataSourceLoader.cs +++ b/net/DevExtreme.AspNet.Data/DataSourceLoader.cs @@ -1,4 +1,5 @@ -using DevExtreme.AspNet.Data.ResponseModel; +using AutoMapper; +using DevExtreme.AspNet.Data.ResponseModel; using System; using System.Collections.Generic; using System.Linq; @@ -24,6 +25,19 @@ public static LoadResult Load(IEnumerable source, DataSourceLoadOptionsBas return Load(source.AsQueryable(), options); } + /// + /// Loads data from a collection that implements the interface. + /// + /// The type of objects in the collection. + /// The type of objects the result will be projected to. + /// A collection that implements the interface. + /// Data processing settings when loading data. + /// The automapper mapper to use for the projection (IMapper) + /// Optional parameters for injection into the mapping (refer ProjectTo definition) + /// The load result. + public static LoadResult Load(IEnumerable source, DataSourceLoadOptionsBase options, IMapper mapper, object automapperProjectionParameters = null) { + return Load(source.AsQueryable(), options, mapper, automapperProjectionParameters); + } /// /// Loads data from a collection that implements the interface. /// @@ -35,6 +49,20 @@ public static LoadResult Load(IQueryable source, DataSourceLoadOptionsBase return LoadAsync(source, options, CancellationToken.None, true).GetAwaiter().GetResult(); } + /// + /// Loads data from a collection that implements the interface. + /// + /// The type of objects in the collection. + /// The type of objects the result will be projected to. + /// A collection that implements the interface. + /// Data processing settings when loading data. + /// The automapper mapper to use for the projection (IMapper) + /// Optional parameters for injection into the mapping (refer ProjectTo definition) + /// The load result. + public static LoadResult Load(IQueryable source, DataSourceLoadOptionsBase options, IMapper mapper, object automapperProjectionParameters = null) { + return LoadAsync(source, options, CancellationToken.None, true, mapper, automapperProjectionParameters).GetAwaiter().GetResult(); + } + /// /// Asynchronously loads data from a collection that implements the interface. /// @@ -50,10 +78,48 @@ public static LoadResult Load(IQueryable source, DataSourceLoadOptionsBase return LoadAsync(source, options, cancellationToken, false); } + /// + /// Asynchronously loads data from a collection that implements the interface. + /// + /// The type of objects in the collection. + /// The type of objects the result will be projected to. + /// A collection that implements the interface. + /// Data processing settings when loading data. + /// The automapper mapper to use for the projection (IMapper) + /// Optional parameters for injection into the mapping (refer ProjectTo definition) + /// + /// A object that represents the asynchronous operation. + /// The task result contains the load result. + /// + public static Task LoadAsync(IQueryable source, DataSourceLoadOptionsBase options, IMapper mapper, object automapperProjectionParameters = null) { + return LoadAsync(source, options, default, false, mapper, automapperProjectionParameters); + } + + /// + /// Asynchronously loads data from a collection that implements the interface. + /// + /// The type of objects in the collection. + /// The type of objects the result will be projected to. + /// A collection that implements the interface. + /// Data processing settings when loading data. + /// A object that delivers a cancellation notice to the running operation. + /// The automapper mapper to use for the projection (IMapper) + /// Optional parameters for injection into the mapping (refer ProjectTo definition) + /// + /// A object that represents the asynchronous operation. + /// The task result contains the load result. + /// + public static Task LoadAsync(IQueryable source, DataSourceLoadOptionsBase options, CancellationToken cancellationToken, IMapper mapper, object automapperProjectionParameters = null) { + return LoadAsync(source, options, cancellationToken, false, mapper, automapperProjectionParameters); + } + static Task LoadAsync(IQueryable source, DataSourceLoadOptionsBase options, CancellationToken ct, bool sync) { - return new DataSourceLoaderImpl(source, options, ct, sync).LoadAsync(); + return new DataSourceLoaderImpl(source, options, ct, sync).LoadAsync(); } + static Task LoadAsync(IQueryable source, DataSourceLoadOptionsBase options, CancellationToken ct, bool sync, IMapper mapper, object automapperProjectionParameters = null) { + return new DataSourceLoaderImpl(source, options, ct, sync, mapper, automapperProjectionParameters).LoadAsync(); + } } } diff --git a/net/DevExtreme.AspNet.Data/DataSourceLoaderImpl.cs b/net/DevExtreme.AspNet.Data/DataSourceLoaderImpl.cs index b6df561d..40b2e0b7 100644 --- a/net/DevExtreme.AspNet.Data/DataSourceLoaderImpl.cs +++ b/net/DevExtreme.AspNet.Data/DataSourceLoaderImpl.cs @@ -1,4 +1,5 @@ -using DevExtreme.AspNet.Data.Aggregation; +using AutoMapper; +using DevExtreme.AspNet.Data.Aggregation; using DevExtreme.AspNet.Data.Async; using DevExtreme.AspNet.Data.Helpers; using DevExtreme.AspNet.Data.RemoteGrouping; @@ -25,13 +26,13 @@ class DataSourceLoaderImpl { readonly bool UseEnumerableOnce; #endif - public DataSourceLoaderImpl(IQueryable source, DataSourceLoadOptionsBase options, CancellationToken cancellationToken, bool sync) { + public DataSourceLoaderImpl(IQueryable source, DataSourceLoadOptionsBase options, CancellationToken cancellationToken, bool sync, IMapper mapper = null, object automapperProjectionParameters = null) { var providerInfo = new QueryProviderInfo(source.Provider); Source = source; - Context = new DataSourceLoadContext(options, providerInfo, Source.ElementType); + Context = new DataSourceLoadContext(options, providerInfo, Source.ElementType, mapper); + Context.AutomapperProjectionParameters = automapperProjectionParameters; CreateExecutor = expr => new ExpressionExecutor(Source.Provider, expr, providerInfo, cancellationToken, sync, options.AllowAsyncOverSync); - #if DEBUG ExpressionWatcher = options.ExpressionWatcher; UseEnumerableOnce = options.UseEnumerableOnce; @@ -40,7 +41,9 @@ public DataSourceLoaderImpl(IQueryable source, DataSourceLoadOptionsBase options DataSourceExpressionBuilder CreateBuilder() => new DataSourceExpressionBuilder(Source.Expression, Context); - public async Task LoadAsync() { + Type dtoType = null; + public async Task LoadAsync() { + dtoType = typeof(TDto) == typeof(S) ? null : typeof(TDto); if(Context.IsCountQuery) return new LoadResult { totalCount = await ExecTotalCountAsync() }; @@ -92,9 +95,9 @@ public async Task LoadAsync() { var loadKeysExpr = CreateBuilder().BuildLoadExpr(true, selectOverride: Context.PrimaryKey); var keyTuples = await ExecExprAnonAsync(loadKeysExpr); - loadExpr = CreateBuilder().BuildLoadExpr(false, filterOverride: FilterFromKeys(keyTuples)); + loadExpr = CreateBuilder().BuildLoadExpr(false, filterOverride: FilterFromKeys(keyTuples), projectionType: dtoType); } else { - loadExpr = CreateBuilder().BuildLoadExpr(!deferPaging); + loadExpr = CreateBuilder().BuildLoadExpr(!deferPaging, projectionType: dtoType); } if(Context.HasAnySelect) { @@ -102,6 +105,11 @@ await ContinueWithGroupingAsync( await ExecWithSelectAsync(loadExpr), result ); + } else if(Context.HasProjection) { + await ContinueWithGroupingAsync( + await ExecExprAsync(loadExpr), + result + ); } else { await ContinueWithGroupingAsync( await ExecExprAsync(loadExpr), @@ -126,7 +134,7 @@ async Task LoadAggregatesOnlyAsync() { await ContinueWithAggregationAsync(null, null, result, false); } else { var data = await ExecExprAsync(CreateBuilder().BuildLoadExpr(false)); - await ContinueWithAggregationAsync(data, new DefaultAccessor(), result, false); + await ContinueWithAggregationAsync(data, new DefaultAccessor(Context.AutomapperProjectionParameters), result, false); } return result; @@ -136,11 +144,11 @@ async Task> ExecWithSelectAsync(Expression loadExpr) if(Context.UseRemoteSelect) return SelectHelper.ConvertRemoteResult(await ExecExprAnonAsync(loadExpr), Context.FullSelect); - return SelectHelper.Evaluate(await ExecExprAsync(loadExpr), Context.FullSelect); + return SelectHelper.Evaluate(await ExecExprAsync(loadExpr), Context.FullSelect, Context.AutomapperProjectionParameters); } async Task ContinueWithGroupingAsync(IEnumerable loadResult, LoadResult result) { - var accessor = new DefaultAccessor(); + var accessor = new DefaultAccessor(Context.AutomapperProjectionParameters); if(Context.HasGroups) { var groups = new GroupHelper(accessor).Group(loadResult, Context.Group); if(Context.RequireGroupCount) @@ -170,7 +178,7 @@ async Task ContinueWithAggregationAsync(IEnumerable data, IAccessor access } else if(Context.HasSummary) { if(includeData) data = Buffer(data); - result.summary = new AggregateCalculator(data, accessor, Context.TotalSummary, Context.GroupSummary).Run(); + result.summary = new AggregateCalculator(data, accessor, Context.TotalSummary, Context.GroupSummary, null, Context.AutomapperProjectionParameters).Run(); } } @@ -198,10 +206,11 @@ Task ExecCountAsync(Expression expr) { async Task ExecRemoteGroupingAsync(bool remotePaging, bool suppressGroups, bool suppressTotals) { return RemoteGroupTransformer.Run( Source.ElementType, - await ExecExprAnonAsync(CreateBuilder().BuildLoadGroupsExpr(remotePaging, suppressGroups, suppressTotals)), + await ExecExprAnonAsync(CreateBuilder().BuildLoadGroupsExpr(remotePaging, suppressGroups, suppressTotals, dtoType)), !suppressGroups && Context.HasGroups ? Context.Group.Count : 0, !suppressTotals ? Context.TotalSummary : null, - !suppressGroups ? Context.GroupSummary : null + !suppressGroups ? Context.GroupSummary : null, + Context.AutomapperProjectionParameters ); } diff --git a/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj b/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj index 3e73d07b..5f1ae314 100644 --- a/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj +++ b/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj @@ -1,28 +1,29 @@  - 99.0 - DevExtreme.AspNet.Data - %meta_description% - %meta_company% - %meta_copyright% - %meta_project_url% + 1.16 + DXAutomap.AspNet.Data + DevExtreme data layer extension for ASP.NET supporting Automapper + Devexpress_Statler + https://github.com/statler/DevExtreme.AspNet.Data MIT https://secure.gravatar.com/avatar/6b38f1e9ffd8b069bcdc2741934fdbcf?s=512&r=g - DevExtreme.AspNet.Data - 99.0 - net461;net6.0 + DXAutomap.AspNet.Data + 1.16 + net7.0 full bin\Debug\$(TargetFramework)\DevExtreme.AspNet.Data.xml 1591 + 1.16.0.0 - - - + + + + diff --git a/net/DevExtreme.AspNet.Data/ExpressionCompiler.cs b/net/DevExtreme.AspNet.Data/ExpressionCompiler.cs index b9bede5b..9f06fe3b 100644 --- a/net/DevExtreme.AspNet.Data/ExpressionCompiler.cs +++ b/net/DevExtreme.AspNet.Data/ExpressionCompiler.cs @@ -9,17 +9,20 @@ namespace DevExtreme.AspNet.Data { - abstract class ExpressionCompiler { + public abstract class ExpressionCompiler { protected readonly Type ItemType; protected readonly bool GuardNulls; + public object RuntimeResolutionContext; - public ExpressionCompiler(Type itemType, bool guardNulls) { + + public ExpressionCompiler(Type itemType, bool guardNulls, object runtimeResolutionContext) { ItemType = itemType; GuardNulls = guardNulls; + RuntimeResolutionContext = runtimeResolutionContext; } protected internal Expression CompileAccessorExpression(Expression target, string clientExpr, Action> customizeProgression = null, bool liftToNullable = false) { - var customResult = CustomAccessorCompilers.TryCompile(target, clientExpr); + var customResult = CustomAccessorCompilers.TryCompile(target, clientExpr, RuntimeResolutionContext); if(customResult != null) return customResult; @@ -43,8 +46,13 @@ protected internal Expression CompileAccessorExpression(Expression target, strin currentTarget = ReadExpando(currentTarget, clientExprItem); else if(DynamicBindingHelper.ShouldUseDynamicBinding(currentTarget.Type)) currentTarget = DynamicBindingHelper.CompileGetMember(currentTarget, clientExprItem); - else - currentTarget = FixReflectedType(GetPropertyOrField(currentTarget, clientExprItem)); + else { + var customResultSplit = CustomAccessorCompilers.TryCompile(target, clientExprItem, RuntimeResolutionContext); + if(customResultSplit != null) { + currentTarget = customResultSplit; + } + else currentTarget = FixReflectedType(GetPropertyOrField(currentTarget, clientExprItem)); + } progression.Add(currentTarget); } @@ -95,7 +103,7 @@ Expression CompileNullGuard(IEnumerable progression) { ); } - protected ParameterExpression CreateItemParam() { + public ParameterExpression CreateItemParam() { return Expression.Parameter(ItemType, "obj"); } diff --git a/net/DevExtreme.AspNet.Data/FilterExpressionCompiler.cs b/net/DevExtreme.AspNet.Data/FilterExpressionCompiler.cs index 064ced25..4df86a16 100644 --- a/net/DevExtreme.AspNet.Data/FilterExpressionCompiler.cs +++ b/net/DevExtreme.AspNet.Data/FilterExpressionCompiler.cs @@ -10,7 +10,7 @@ namespace DevExtreme.AspNet.Data { - class FilterExpressionCompiler : ExpressionCompiler { + public class FilterExpressionCompiler : ExpressionCompiler { const string CONTAINS = "contains", NOT_CONTAINS = "notcontains", @@ -21,9 +21,10 @@ const string readonly bool _supportsEqualsMethod; - public FilterExpressionCompiler(Type itemType, bool guardNulls, bool stringToLower = false, bool supportsEqualsMethod = true) - : base(itemType, guardNulls) { + public FilterExpressionCompiler(Type itemType, bool guardNulls, bool stringToLower = false, bool supportsEqualsMethod = true, object runtimeResolutionContext = null) + : base(itemType, guardNulls, runtimeResolutionContext) { _stringToLower = stringToLower; + RuntimeResolutionContext = runtimeResolutionContext; _supportsEqualsMethod = supportsEqualsMethod; } @@ -43,7 +44,17 @@ Expression CompileCore(ParameterExpression dataItemExpr, IList criteriaJson) { return CompileBinary(dataItemExpr, criteriaJson); } - Expression CompileBinary(ParameterExpression dataItemExpr, IList criteriaJson) { + public Expression CompileNonCustomBinary(ParameterExpression dataItemExpr, IList criteria) { + + return CompileBinary(dataItemExpr, criteria, false); + } + + public Expression CompileNonCustomBinary(IList criteria) { + + return CompileBinary(CreateItemParam(), criteria, false); + } + + Expression CompileBinary(ParameterExpression dataItemExpr, IList criteriaJson, bool shouldProcessCustom = true) { var hasExplicitOperation = criteriaJson.Count > 2; var clientAccessor = Convert.ToString(criteriaJson[0]); @@ -51,14 +62,14 @@ Expression CompileBinary(ParameterExpression dataItemExpr, IList criteriaJson) { var clientValue = Utils.UnwrapNewtonsoftValue(criteriaJson[hasExplicitOperation ? 2 : 1]); var isStringOperation = clientOperation == CONTAINS || clientOperation == NOT_CONTAINS || clientOperation == STARTS_WITH || clientOperation == ENDS_WITH; - if(CustomFilterCompilers.Binary.CompilerFuncs.Count > 0) { + if(shouldProcessCustom && CustomFilterCompilers.Binary.CompilerFuncsWithContext.Count > 0) { var customResult = CustomFilterCompilers.Binary.TryCompile(new BinaryExpressionInfo { DataItemExpression = dataItemExpr, AccessorText = clientAccessor, Operation = clientOperation, Value = clientValue, StringToLower = _stringToLower - }); + }, this); if(customResult != null) return customResult; diff --git a/net/DevExtreme.AspNet.Data/Helpers/CustomAccessorCompilers.cs b/net/DevExtreme.AspNet.Data/Helpers/CustomAccessorCompilers.cs index 8d844d65..1e2a2710 100644 --- a/net/DevExtreme.AspNet.Data/Helpers/CustomAccessorCompilers.cs +++ b/net/DevExtreme.AspNet.Data/Helpers/CustomAccessorCompilers.cs @@ -1,28 +1,86 @@ -using System; +using AutoMapper; +using AutoMapper.Internal; +using System; +using System.Collections.Concurrent; using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; +using System.Collections.ObjectModel; using System.Linq.Expressions; namespace DevExtreme.AspNet.Data.Helpers { public static class CustomAccessorCompilers { public delegate Expression CompilerFunc(Expression expr, string accessorText); + public delegate Expression CompilerFuncWithContext(Expression expr, string accessorText, object runtimeContext); - static readonly ICollection _compilers = new List(); + static readonly ICollection _compilersWithContext = new List(); + + private static AccessorLibrary CustomAccessorLibrary = new AccessorLibrary(); public static void Register(CompilerFunc compilerFunc) { - _compilers.Add(compilerFunc); + _compilersWithContext.Add((target, accessorText, runtimeContext) => compilerFunc(target, accessorText)); + } + public static void RegisterWithContext(CompilerFuncWithContext compilerFunc) { + _compilersWithContext.Add(compilerFunc); + } + + /// + /// + /// + /// + /// + /// + /// + public static void Register(string PropertyName, Expression> ResolveExpression) { + CustomAccessorLibrary.Add(PropertyName, ResolveExpression); + } + public static void RegisterContext(string PropertyName, Func>> ResolveExprFunc) { + CustomAccessorLibrary.Add(PropertyName, ResolveExprFunc); + } + + public static void RegisterContext(string PropertyName, Func ResolveExprFunc) { + CustomAccessorLibrary.Add(PropertyName, ResolveExprFunc); + } + + public static void RegisterAutomapperProfiles(IMapper mapper) { + + //Get all automapper config maps and create expressions that can be parsed by devex.aspnet.data + //Create a dictionary of dictionaries so they can be (efficiently) extracted by the devex library + //by type and property name + var allTypeMaps = mapper.ConfigurationProvider.Internal().GetAllTypeMaps(); + foreach(TypeMap map in allTypeMaps) { + var propertyMaps = map.PropertyMaps; + + foreach(PropertyMap propertyMap in propertyMaps) { + string modelMemberType = propertyMap?.TypeMap?.SourceType?.Name; + string destinationName = propertyMap?.DestinationName; + var exp = propertyMap.CustomMapExpression; + if(modelMemberType != null && destinationName != null && exp == null) continue; + + CustomAccessorLibrary.Add(modelMemberType, destinationName, exp); + } + } + RegisterAccessorFinder(); } - internal static Expression TryCompile(Expression expr, string accessorText) { - if(_compilers.Count < 1) + private static void RegisterAccessorFinder() { + + //Register the method that finds the right accessor from the dictionary. This will provide + //an accessor for the registered Automapper and + RegisterWithContext((target, accessorText, runtimeContext) => { + var accessor = CustomAccessorLibrary.Get(target, target.Type.Name, accessorText, runtimeContext); + if(accessor != null) return accessor; + return null; + }); + } - foreach(var compiler in _compilers) { - var result = compiler(expr, accessorText); - if(result != null) - return result; + internal static Expression TryCompile(Expression expr, string accessorText, object runtimeContext) { + if(_compilersWithContext.Count >= 1) { + foreach(var compiler in _compilersWithContext) { + var result = compiler(expr, accessorText, runtimeContext); + if(result != null) + return result; + } } return null; @@ -30,10 +88,103 @@ internal static Expression TryCompile(Expression expr, string accessorText) { #if DEBUG internal static void Clear() { - _compilers.Clear(); + _compilersWithContext.Clear(); } #endif } + public class AccessorLibrary { + ConcurrentDictionary> _dctAccessors = new ConcurrentDictionary>(); + public AccessorLibrary() { + } + + public void Add(string TypeName, string PropertyName, LambdaExpression ResolveExpression) { + var _accessor = new Accessor(TypeName, PropertyName, ResolveExpression); + if(_dctAccessors.ContainsKey(TypeName)) { + var expressionForType = _dctAccessors[TypeName]; + expressionForType[PropertyName] = _accessor; + } else { + var accessorDct = new ConcurrentDictionary() { [PropertyName] = _accessor }; + _dctAccessors.TryAdd(TypeName, accessorDct); + } + } + + public void Add(string TypeName, string PropertyName, Func ResolveExprFunc) { + var _accessor = new Accessor(TypeName, PropertyName, ResolveExprFunc); + if(_dctAccessors.ContainsKey(TypeName)) { + var expressionForType = _dctAccessors[TypeName]; + expressionForType[PropertyName] = _accessor; + } else { + var accessorDct = new ConcurrentDictionary() { [PropertyName] = _accessor }; + _dctAccessors.TryAdd(TypeName, accessorDct); + } + } + public void Add(string PropertyName, Func ResolveExprFunc) { + Add(typeof(T).Name, PropertyName, ResolveExprFunc); + } + + public void Add(string PropertyName, LambdaExpression ResolveExpression) { + Add(typeof(T).Name, PropertyName, ResolveExpression); + } + + public void Add(string PropertyName, Expression> ResolveExpression) { + Add(typeof(T).Name, PropertyName, ResolveExpression); + } + + public Expression Get(Expression target, string TypeName, string PropertyName, object runtimeContext) { + if(_dctAccessors.ContainsKey(TypeName)) { + var expressionForType = _dctAccessors[TypeName]; + if(expressionForType.ContainsKey(PropertyName)) { + var expression = expressionForType[PropertyName].GetResolvedExpression(runtimeContext); + return new ParameterVisitor(expression.Parameters, target as ParameterExpression) + .VisitAndConvert(expression.Body, PropertyName); + } + } + + return null; + } + } + + public class Accessor { + public string TypeName { get; set; } + public string PropertyName { get; set; } + Func ResolveExpression { get; set; } + + public Accessor() { + } + public Accessor(string typeName, string propertyName, LambdaExpression resolveExpression) { + TypeName = typeName; + PropertyName = propertyName; + ResolveExpression = (o) => resolveExpression; + } + public Accessor(string typeName, string propertyName, Func resolveExpressionFunc) { + TypeName = typeName; + PropertyName = propertyName; + ResolveExpression = resolveExpressionFunc; + } + + public LambdaExpression GetResolvedExpression(object runtimeContext) { + return ResolveExpression(runtimeContext); + } + } + + public class ParameterVisitor : ExpressionVisitor { + private readonly ReadOnlyCollection _from; + private readonly ParameterExpression _to; + public ParameterVisitor( + ReadOnlyCollection from, + ParameterExpression to) { + if(from == null) throw new ArgumentNullException("from"); + if(to == null) throw new ArgumentNullException("to"); + this._from = from; + this._to = to; + } + protected override Expression VisitParameter(ParameterExpression node) { + for(int i = 0; i < _from.Count; i++) { + if(node == _from[i]) return _to; + } + return node; + } + } } diff --git a/net/DevExtreme.AspNet.Data/Helpers/CustomFilterCompilers.cs b/net/DevExtreme.AspNet.Data/Helpers/CustomFilterCompilers.cs index a23c4f0a..130e3b75 100644 --- a/net/DevExtreme.AspNet.Data/Helpers/CustomFilterCompilers.cs +++ b/net/DevExtreme.AspNet.Data/Helpers/CustomFilterCompilers.cs @@ -5,15 +5,16 @@ namespace DevExtreme.AspNet.Data.Helpers { using BinaryExpressionCompilerFunc = Func; + using BinaryExpressionCompilerWithContextFunc = Func; public static class CustomFilterCompilers { internal static class Binary { - internal readonly static ICollection CompilerFuncs = new List(); + internal readonly static ICollection CompilerFuncsWithContext = new List(); - internal static Expression TryCompile(IBinaryExpressionInfo info) { - foreach(var func in CompilerFuncs) { - var result = func(info); + internal static Expression TryCompile(IBinaryExpressionInfo info, FilterExpressionCompiler filterExpressionCompiler) { + foreach(var func in CompilerFuncsWithContext) { + var result = func(info, filterExpressionCompiler); if(result != null) return result; } @@ -22,9 +23,12 @@ internal static Expression TryCompile(IBinaryExpressionInfo info) { } public static void RegisterBinaryExpressionCompiler(BinaryExpressionCompilerFunc compilerFunc) { - Binary.CompilerFuncs.Add(compilerFunc); + Binary.CompilerFuncsWithContext.Add((item, _cx) => compilerFunc(item)); } + public static void RegisterBinaryExpressionCompilerWithContext(BinaryExpressionCompilerWithContextFunc compilerFunc) { + Binary.CompilerFuncsWithContext.Add(compilerFunc); + } } } diff --git a/net/DevExtreme.AspNet.Data/Helpers/DataSourceLoadOptionsParser.cs b/net/DevExtreme.AspNet.Data/Helpers/DataSourceLoadOptionsParser.cs index 85403e0f..f8caba1a 100644 --- a/net/DevExtreme.AspNet.Data/Helpers/DataSourceLoadOptionsParser.cs +++ b/net/DevExtreme.AspNet.Data/Helpers/DataSourceLoadOptionsParser.cs @@ -23,7 +23,8 @@ public const string KEY_FILTER = "filter", KEY_TOTAL_SUMMARY = "totalSummary", KEY_GROUP_SUMMARY = "groupSummary", - KEY_SELECT = "select"; + KEY_SELECT = "select", + KEY_PROJECTBEFOREFILTER = "projectbeforefilter"; /// /// Converts the string representations of the data processing settings to equivalent values of appropriate types. @@ -42,6 +43,7 @@ public static void Parse(DataSourceLoadOptionsBase loadOptions, Func(groupSummary, DEFAULT_SERIALIZER_OPTIONS); if(!String.IsNullOrEmpty(select)) - loadOptions.Select = JsonSerializer.Deserialize(select); + loadOptions.Select = JsonSerializer.Deserialize(select); + + if(!String.IsNullOrEmpty(projectbeforefilter)) + loadOptions.ProjectBeforeFilter = Convert.ToBoolean(isCountQuery); } } diff --git a/net/DevExtreme.AspNet.Data/Helpers/DefaultAccessor.cs b/net/DevExtreme.AspNet.Data/Helpers/DefaultAccessor.cs index 72dadd1e..b291f121 100644 --- a/net/DevExtreme.AspNet.Data/Helpers/DefaultAccessor.cs +++ b/net/DevExtreme.AspNet.Data/Helpers/DefaultAccessor.cs @@ -8,8 +8,8 @@ namespace DevExtreme.AspNet.Data.Helpers { class DefaultAccessor : ExpressionCompiler, IAccessor { IDictionary> _accessors; - public DefaultAccessor() - : base(typeof(T), true) { + public DefaultAccessor(object runtimeResolutionContext) + : base(typeof(T), true, runtimeResolutionContext) { } public object Read(T obj, string selector) { diff --git a/net/DevExtreme.AspNet.Data/RemoteGrouping/RemoteGroupExpressionCompiler.cs b/net/DevExtreme.AspNet.Data/RemoteGrouping/RemoteGroupExpressionCompiler.cs index 083ee32b..daa3fbb6 100644 --- a/net/DevExtreme.AspNet.Data/RemoteGrouping/RemoteGroupExpressionCompiler.cs +++ b/net/DevExtreme.AspNet.Data/RemoteGrouping/RemoteGroupExpressionCompiler.cs @@ -17,8 +17,10 @@ class RemoteGroupExpressionCompiler : ExpressionCompiler { _totalSummary, _groupSummary; - public RemoteGroupExpressionCompiler(Type itemType, bool guardNulls, bool expandSumType, AnonTypeNewTweaks anonTypeNewTweaks, IEnumerable grouping, IEnumerable totalSummary, IEnumerable groupSummary) - : base(itemType, guardNulls) { + public RemoteGroupExpressionCompiler(Type itemType, bool guardNulls, bool expandSumType, AnonTypeNewTweaks anonTypeNewTweaks, + IEnumerable grouping, IEnumerable totalSummary, IEnumerable groupSummary, + object runtimeResolutionContext) + : base(itemType, guardNulls, runtimeResolutionContext) { _expandSumType = expandSumType; _anonTypeNewTweaks = anonTypeNewTweaks; _grouping = grouping; @@ -27,8 +29,8 @@ public RemoteGroupExpressionCompiler(Type itemType, bool guardNulls, bool expand } #if DEBUG - public RemoteGroupExpressionCompiler(Type itemType, IEnumerable grouping, IEnumerable totalSummary, IEnumerable groupSummary) - : this(itemType, false, false, null, grouping, totalSummary, groupSummary) { + public RemoteGroupExpressionCompiler(Type itemType, IEnumerable grouping, IEnumerable totalSummary, IEnumerable groupSummary, object runtimeResolutionContext) + : this(itemType, false, false, null, grouping, totalSummary, groupSummary, runtimeResolutionContext) { } #endif diff --git a/net/DevExtreme.AspNet.Data/RemoteGrouping/RemoteGroupTransformer.cs b/net/DevExtreme.AspNet.Data/RemoteGrouping/RemoteGroupTransformer.cs index d3fbd76c..b24e7e51 100644 --- a/net/DevExtreme.AspNet.Data/RemoteGrouping/RemoteGroupTransformer.cs +++ b/net/DevExtreme.AspNet.Data/RemoteGrouping/RemoteGroupTransformer.cs @@ -11,7 +11,8 @@ namespace DevExtreme.AspNet.Data.RemoteGrouping { class RemoteGroupTransformer { - public static RemoteGroupingResult Run(Type sourceItemType, IEnumerable flatGroups, int groupCount, IReadOnlyList totalSummary, IReadOnlyList groupSummary) { + public static RemoteGroupingResult Run(Type sourceItemType, IEnumerable flatGroups, int groupCount, IReadOnlyList totalSummary, + IReadOnlyList groupSummary, object runtimeResolutionContext) { List hierGroups = null; if(groupCount > 0) { @@ -32,8 +33,8 @@ public static RemoteGroupingResult Run(Type sourceItemType, IEnumerable(); transformedTotalSummary.Add(new SummaryInfo { SummaryType = AggregateName.REMOTE_COUNT }); - var sumFix = new SumFix(sourceItemType, totalSummary, groupSummary); - var totals = new AggregateCalculator(dataToAggregate, AnonTypeAccessor.Instance, transformedTotalSummary, transformedGroupSummary, sumFix).Run(); + var sumFix = new SumFix(sourceItemType, totalSummary, groupSummary, runtimeResolutionContext); + var totals = new AggregateCalculator(dataToAggregate, AnonTypeAccessor.Instance, transformedTotalSummary, transformedGroupSummary, sumFix, runtimeResolutionContext).Run(); var totalCount = (int)totals.Last(); totals = totals.Take(totals.Length - 1).ToArray(); diff --git a/net/DevExtreme.AspNet.Data/SelectExpressionCompiler.cs b/net/DevExtreme.AspNet.Data/SelectExpressionCompiler.cs index 02bbb43e..22ecdb81 100644 --- a/net/DevExtreme.AspNet.Data/SelectExpressionCompiler.cs +++ b/net/DevExtreme.AspNet.Data/SelectExpressionCompiler.cs @@ -11,8 +11,8 @@ namespace DevExtreme.AspNet.Data { class SelectExpressionCompiler : ExpressionCompiler { AnonTypeNewTweaks _anonTypeNewTweaks; - public SelectExpressionCompiler(Type itemType, bool guardNulls, AnonTypeNewTweaks anonTypeNewTweaks = null) - : base(itemType, guardNulls) { + public SelectExpressionCompiler(Type itemType, bool guardNulls, AnonTypeNewTweaks anonTypeNewTweaks = null, object runtimeResolutionContext = null) + : base(itemType, guardNulls, runtimeResolutionContext) { _anonTypeNewTweaks = anonTypeNewTweaks; } diff --git a/net/DevExtreme.AspNet.Data/SelectHelper.cs b/net/DevExtreme.AspNet.Data/SelectHelper.cs index 1f189039..3b8336de 100644 --- a/net/DevExtreme.AspNet.Data/SelectHelper.cs +++ b/net/DevExtreme.AspNet.Data/SelectHelper.cs @@ -9,10 +9,10 @@ namespace DevExtreme.AspNet.Data { static class SelectHelper { - public static IEnumerable Evaluate(IEnumerable data, IEnumerable select) { + public static IEnumerable Evaluate(IEnumerable data, IEnumerable select, object runtimeResolutionContext) { var bufferedSelect = select.ToArray(); var paths = SelectToPaths(bufferedSelect); - var accessor = new DefaultAccessor(); + var accessor = new DefaultAccessor(runtimeResolutionContext); foreach(var item in data) yield return PathsToExpando(paths, i => accessor.Read(item, bufferedSelect[i])); diff --git a/net/DevExtreme.AspNet.Data/SortExpressionCompiler.cs b/net/DevExtreme.AspNet.Data/SortExpressionCompiler.cs index 250384f2..9fa6e914 100644 --- a/net/DevExtreme.AspNet.Data/SortExpressionCompiler.cs +++ b/net/DevExtreme.AspNet.Data/SortExpressionCompiler.cs @@ -8,8 +8,8 @@ namespace DevExtreme.AspNet.Data { class SortExpressionCompiler : ExpressionCompiler { - public SortExpressionCompiler(Type itemType, bool guardNulls) - : base(itemType, guardNulls) { + public SortExpressionCompiler(Type itemType, bool guardNulls, object runtimeResolutionContext) + : base(itemType, guardNulls, runtimeResolutionContext) { } public Expression Compile(Expression target, IEnumerable clientExprList) { diff --git a/net/DevExtreme.AspNet.Data/release.snk.enc b/net/DevExtreme.AspNet.Data/release.snk.enc deleted file mode 100644 index df8ac2b0..00000000 Binary files a/net/DevExtreme.AspNet.Data/release.snk.enc and /dev/null differ diff --git a/net/Sample/Sample.csproj b/net/Sample/Sample.csproj index 77c36022..03fc910b 100644 --- a/net/Sample/Sample.csproj +++ b/net/Sample/Sample.csproj @@ -1,7 +1,7 @@  - net6.0;net7.0;net8.0 + net7.0;net8.0 enable @@ -11,15 +11,6 @@ - - NET6.0 - - - - - - - NET7.0