From 344745b534718cd92b78f6b00e17e4ce1e0f6bc2 Mon Sep 17 00:00:00 2001 From: "dennis.gascoigne" <337991+statler@users.noreply.github.com> Date: Thu, 6 Apr 2023 20:57:29 +1000 Subject: [PATCH 01/23] - Added RuntimeResolutionContext to allow passing in variables which will affect the CustomFilterCompiler execution - Add new CompilerFuncsWithContext to support RuntimeResolutionContext without changing signatures for existing users - Make FilterExpressionCompiler public and add public method to support leveraging CompileBinary method. This allows complex operator resolution without duplicating the devex operations. --- .../DataSourceExpressionBuilder.cs | 2 +- .../DataSourceLoadContext.cs | 6 +++++ .../DataSourceLoadOptionsBase.cs | 2 ++ .../ExpressionCompiler.cs | 4 ++-- .../FilterExpressionCompiler.cs | 22 ++++++++++++++----- .../Helpers/CustomFilterCompilers.cs | 12 +++++++++- 6 files changed, 39 insertions(+), 9 deletions(-) diff --git a/net/DevExtreme.AspNet.Data/DataSourceExpressionBuilder.cs b/net/DevExtreme.AspNet.Data/DataSourceExpressionBuilder.cs index a21a003d..2a344f9f 100644 --- a/net/DevExtreme.AspNet.Data/DataSourceExpressionBuilder.cs +++ b/net/DevExtreme.AspNet.Data/DataSourceExpressionBuilder.cs @@ -51,7 +51,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.RuntimeResolutionContext).Compile(filterOverride ?? Context.Filter); Expr = QueryableCall(nameof(Queryable.Where), Expression.Quote(filterExpr)); } diff --git a/net/DevExtreme.AspNet.Data/DataSourceLoadContext.cs b/net/DevExtreme.AspNet.Data/DataSourceLoadContext.cs index aa27ea7c..c4eac913 100644 --- a/net/DevExtreme.AspNet.Data/DataSourceLoadContext.cs +++ b/net/DevExtreme.AspNet.Data/DataSourceLoadContext.cs @@ -307,4 +307,10 @@ string[] Init() { } } } + + + partial class DataSourceLoadContext { + public object RuntimeResolutionContext => _options.RuntimeResolutionContext; + } + } diff --git a/net/DevExtreme.AspNet.Data/DataSourceLoadOptionsBase.cs b/net/DevExtreme.AspNet.Data/DataSourceLoadOptionsBase.cs index 4d6da287..293728f4 100644 --- a/net/DevExtreme.AspNet.Data/DataSourceLoadOptionsBase.cs +++ b/net/DevExtreme.AspNet.Data/DataSourceLoadOptionsBase.cs @@ -118,6 +118,8 @@ public class DataSourceLoadOptionsBase { public bool AllowAsyncOverSync { get; set; } + public object RuntimeResolutionContext { get; set; } + #if DEBUG internal Action ExpressionWatcher; internal bool UseEnumerableOnce; diff --git a/net/DevExtreme.AspNet.Data/ExpressionCompiler.cs b/net/DevExtreme.AspNet.Data/ExpressionCompiler.cs index b9bede5b..8d62d19c 100644 --- a/net/DevExtreme.AspNet.Data/ExpressionCompiler.cs +++ b/net/DevExtreme.AspNet.Data/ExpressionCompiler.cs @@ -9,7 +9,7 @@ namespace DevExtreme.AspNet.Data { - abstract class ExpressionCompiler { + public abstract class ExpressionCompiler { protected readonly Type ItemType; protected readonly bool GuardNulls; @@ -95,7 +95,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..4d120e4c 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", @@ -20,10 +20,12 @@ const string bool _stringToLower; readonly bool _supportsEqualsMethod; + public object RuntimeResolutionContext; - public FilterExpressionCompiler(Type itemType, bool guardNulls, bool stringToLower = false, bool supportsEqualsMethod = true) + public FilterExpressionCompiler(Type itemType, bool guardNulls, bool stringToLower = false, bool supportsEqualsMethod = true, object runtimeResolutionContext = null) : base(itemType, guardNulls) { _stringToLower = stringToLower; + RuntimeResolutionContext = runtimeResolutionContext; _supportsEqualsMethod = supportsEqualsMethod; } @@ -43,7 +45,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 +63,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.CompilerFuncs.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/CustomFilterCompilers.cs b/net/DevExtreme.AspNet.Data/Helpers/CustomFilterCompilers.cs index a23c4f0a..ad0ac4ec 100644 --- a/net/DevExtreme.AspNet.Data/Helpers/CustomFilterCompilers.cs +++ b/net/DevExtreme.AspNet.Data/Helpers/CustomFilterCompilers.cs @@ -5,18 +5,25 @@ 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) { + internal static Expression TryCompile(IBinaryExpressionInfo info, FilterExpressionCompiler filterExpressionCompiler) { foreach(var func in CompilerFuncs) { var result = func(info); if(result != null) return result; } + foreach(var func in CompilerFuncsWithContext) { + var result = func(info, filterExpressionCompiler); + if(result != null) + return result; + } return null; } } @@ -25,6 +32,9 @@ public static void RegisterBinaryExpressionCompiler(BinaryExpressionCompilerFunc Binary.CompilerFuncs.Add(compilerFunc); } + public static void RegisterBinaryExpressionCompilerWithContext(BinaryExpressionCompilerWithContextFunc compilerFunc) { + Binary.CompilerFuncsWithContext.Add(compilerFunc); + } } } From 4a61187add43ee20e8ffba921f56ee3efd3c4b13 Mon Sep 17 00:00:00 2001 From: statler <337991+statler@users.noreply.github.com> Date: Tue, 11 Apr 2023 19:51:37 +1000 Subject: [PATCH 02/23] Update README.md --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index 1787cc5e..e3114c8e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,22 @@ +********************************************************************************************************************************************** + +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. + +********************************************************************************************************************************************** + + + + + + + + + + + + # 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) From 53a5f08256aff701ff03ff3e8089dab9e8dc91c0 Mon Sep 17 00:00:00 2001 From: "dennis.gascoigne" <337991+statler@users.noreply.github.com> Date: Tue, 11 Apr 2023 19:52:30 +1000 Subject: [PATCH 03/23] add projection methods --- net/DevExtreme.AspNet.Data/AssemblyInfo.cs | 5 -- .../DataSourceExpressionBuilder.cs | 40 +++++++--- .../DataSourceLoadContext.cs | 21 ++++-- .../DataSourceLoadOptionsBase.cs | 3 +- .../DataSourceLoader.cs | 70 +++++++++++++++++- .../DataSourceLoaderImpl.cs | 21 ++++-- .../DevExtreme.AspNet.Data.csproj | 23 ++---- .../Helpers/DataSourceLoadOptionsParser.cs | 7 +- net/DevExtreme.AspNet.Data/release.snk.enc | Bin 608 -> 0 bytes 9 files changed, 142 insertions(+), 48 deletions(-) delete mode 100644 net/DevExtreme.AspNet.Data/release.snk.enc 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 2a344f9f..61d5b20f 100644 --- a/net/DevExtreme.AspNet.Data/DataSourceExpressionBuilder.cs +++ b/net/DevExtreme.AspNet.Data/DataSourceExpressionBuilder.cs @@ -1,9 +1,13 @@ -using DevExtreme.AspNet.Data.RemoteGrouping; +using AutoMapper; +using AutoMapper.Internal; +using DevExtreme.AspNet.Data.Async; +using DevExtreme.AspNet.Data.RemoteGrouping; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Reflection; namespace DevExtreme.AspNet.Data { @@ -16,13 +20,23 @@ 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() { @@ -51,7 +65,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, Context.RuntimeResolutionContext).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)); } @@ -62,9 +76,15 @@ void AddSort() { Expr = new SortExpressionCompiler(GetItemType(), Context.GuardNulls).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, null, Array.Empty()); + var qryExpr = qryBase.Projection; + Expr = Expression.Call(typeof(Queryable), nameof(Queryable.Select), new[] { _type, qryExpr.ReturnType }, Expr, Expression.Quote(qryExpr)); + } } void AddPaging() { diff --git a/net/DevExtreme.AspNet.Data/DataSourceLoadContext.cs b/net/DevExtreme.AspNet.Data/DataSourceLoadContext.cs index c4eac913..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? @@ -309,8 +322,4 @@ string[] Init() { } - partial class DataSourceLoadContext { - public object RuntimeResolutionContext => _options.RuntimeResolutionContext; - } - } diff --git a/net/DevExtreme.AspNet.Data/DataSourceLoadOptionsBase.cs b/net/DevExtreme.AspNet.Data/DataSourceLoadOptionsBase.cs index 293728f4..9dfbbc96 100644 --- a/net/DevExtreme.AspNet.Data/DataSourceLoadOptionsBase.cs +++ b/net/DevExtreme.AspNet.Data/DataSourceLoadOptionsBase.cs @@ -117,8 +117,7 @@ public class DataSourceLoadOptionsBase { public bool? SortByPrimaryKey { get; set; } public bool AllowAsyncOverSync { get; set; } - - public object RuntimeResolutionContext { 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..70b1b9d4 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,7 @@ public DataSourceLoaderImpl(IQueryable source, DataSourceLoadOptionsBase options DataSourceExpressionBuilder CreateBuilder() => new DataSourceExpressionBuilder(Source.Expression, Context); - public async Task LoadAsync() { + public async Task LoadAsync() { if(Context.IsCountQuery) return new LoadResult { totalCount = await ExecTotalCountAsync() }; @@ -81,6 +82,7 @@ public async Task LoadAsync() { var deferPaging = Context.HasGroups || !Context.UseRemoteGrouping && !Context.SummaryIsTotalCountOnly && Context.HasSummary; Expression loadExpr; + Type dtoType = typeof(TDto) == typeof(S) ? null : typeof(TDto); if(!deferPaging && Context.PaginateViaPrimaryKey && Context.Take > 0) { if(!Context.HasPrimaryKey) { @@ -92,9 +94,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 +104,11 @@ await ContinueWithGroupingAsync( await ExecWithSelectAsync(loadExpr), result ); + } else if(Context.HasProjection) { + await ContinueWithGroupingAsync( + await ExecExprAsync(loadExpr), + result + ); } else { await ContinueWithGroupingAsync( await ExecExprAsync(loadExpr), diff --git a/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj b/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj index 4e7d5fe2..5975cb99 100644 --- a/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj +++ b/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj @@ -1,37 +1,30 @@  - 99.0 - DevExtreme.AspNet.Data + 999 + DXAutomap.AspNet.Data %meta_description% - %meta_company% - %meta_copyright% + Devexpress_Statler %meta_project_url% MIT https://secure.gravatar.com/avatar/6b38f1e9ffd8b069bcdc2741934fdbcf?s=512&r=g - DevExtreme.AspNet.Data - 99.0 - net452;netstandard2.0 + DXAutomap.AspNet.Data + 999 + netstandard2.1 full bin\Debug\$(TargetFramework)\DevExtreme.AspNet.Data.xml 1591 + + - - - - - - - - diff --git a/net/DevExtreme.AspNet.Data/Helpers/DataSourceLoadOptionsParser.cs b/net/DevExtreme.AspNet.Data/Helpers/DataSourceLoadOptionsParser.cs index e9aef280..879515e9 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(select); + + if(!String.IsNullOrEmpty(projectbeforefilter)) + loadOptions.ProjectBeforeFilter = Convert.ToBoolean(isCountQuery); } } diff --git a/net/DevExtreme.AspNet.Data/release.snk.enc b/net/DevExtreme.AspNet.Data/release.snk.enc deleted file mode 100644 index df8ac2b0793be121bde85254089baf4258e7c291..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 608 zcmV-m0-yZ=0Pn`=7D@^Ee2gcHJ2X;a@W@>fg?J|K*^5HWueIcfxivV@b~*c98-~aJ z<6@5A!#*c9pFz6+AOK+zjQ6cT*?kP`1l>*6CA!6urlSIdx8sWy@Gh({wnr4QH)!Y% zxlrT?uV&*M{`!fg`maCB)9YIIQu(<^Rsc9yNZz%DBYOA3b0vC2oWV0Kv#jwjj+*r7 z`yT+dgeJ-;M2rm`+&A%32DV=`oYY2R5wi+&GpzWXg_A_MzjS zD+{|?0486UsBZj}#)3xi~8+%ZI@yb@CZd)N|BvdX2c(z~jS zDFod~AxK#tFDJ`$o`Z8{C(gmG5cAN>6-=jg%3^zF!+)86V#m6kI0aGqBi#_hD?Z=D#`j3$v*qCn*1N){&06-tN9QQb^l7KAMTBcW0c{PzNSx~L}aG64J z3YYV6vDIOICUs=FlFSqy%E$C;lWcu`>Y9Dhok!al7A;dlyG_;e-bbIa6)7nUPL#6%Z;uCK9TVL$sWq`}*)>@xs!gtO{6vTu}^m>x7?J?ru zlSUXZ_f5wzUb#x3&^$-^)XJ~>8 Date: Tue, 11 Apr 2023 21:02:29 +1000 Subject: [PATCH 04/23] Add registration of devex maps --- .../Helpers/CustomAccessorCompilers.cs | 118 +++++++++++++++++- 1 file changed, 117 insertions(+), 1 deletion(-) diff --git a/net/DevExtreme.AspNet.Data/Helpers/CustomAccessorCompilers.cs b/net/DevExtreme.AspNet.Data/Helpers/CustomAccessorCompilers.cs index 8d844d65..6b5319e6 100644 --- a/net/DevExtreme.AspNet.Data/Helpers/CustomAccessorCompilers.cs +++ b/net/DevExtreme.AspNet.Data/Helpers/CustomAccessorCompilers.cs @@ -1,5 +1,8 @@ -using System; +using AutoMapper; +using AutoMapper.Internal; +using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Linq.Expressions; @@ -11,10 +14,56 @@ public static class CustomAccessorCompilers { static readonly ICollection _compilers = new List(); + private static AccessorLibrary CustomAccessorLibrary = new AccessorLibrary(); + public static void Register(CompilerFunc compilerFunc) { _compilers.Add(compilerFunc); } + /// + /// + /// + /// + /// + /// + /// + public static void Register(string PropertyName, Expression> ResolveExpression) { + CustomAccessorLibrary.Add(PropertyName, ResolveExpression); + } + + 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(); + } + + 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 + Register((target, accessorText) => { + var accessor = CustomAccessorLibrary.Get(target, target.Type.Name, accessorText); + if(accessor != null) return accessor; + + return null; + }); + } + internal static Expression TryCompile(Expression expr, string accessorText) { if(_compilers.Count < 1) return null; @@ -36,4 +85,71 @@ internal static void Clear() { } + public class AccessorLibrary { + Dictionary> _dctAccessors = new Dictionary>(); + 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 _dctAccessors.Add(TypeName, new Dictionary() { [PropertyName] = _accessor }); + } + + 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) { + if(_dctAccessors.ContainsKey(TypeName)) { + var expressionForType = _dctAccessors[TypeName]; + if(expressionForType.ContainsKey(PropertyName)) { + var expression = expressionForType[PropertyName].ResolveExpression; + 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; } + public LambdaExpression ResolveExpression { get; set; } + public Accessor() { + } + public Accessor(string typeName, string propertyName, LambdaExpression resolveExpression) { + TypeName = typeName; + PropertyName = propertyName; + ResolveExpression = resolveExpression; + } + } + + 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; + } + } } From 0b5eca233c66a9a8a5f7c35ff051a056918f334a Mon Sep 17 00:00:00 2001 From: statler <337991+statler@users.noreply.github.com> Date: Tue, 11 Apr 2023 21:47:35 +1000 Subject: [PATCH 05/23] Update README.md --- README.md | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/README.md b/README.md index e3114c8e..2c7d3f12 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,109 @@ Its main shortcoming in my use case was a lack of support for Automapper style p ********************************************************************************************************************************************** +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. 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. +3. Specify that filters apply after a projection using the new DataSourceLoadOption property ProjectBeforeFilter +4. Utilise the devex expression builders for binary expressions by calling CompileNonCustomBinary +********************************************************************************************************************************************** + +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()); + + .... + } + +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) + +********************************************************************************************************************************************** From a51bde59f9c0db363d77fcc3fe16fc85094105de Mon Sep 17 00:00:00 2001 From: "dennis.gascoigne" <337991+statler@users.noreply.github.com> Date: Wed, 12 Apr 2023 08:34:08 +1000 Subject: [PATCH 06/23] Update config for publish --- net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj b/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj index 5975cb99..1c101c8f 100644 --- a/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj +++ b/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj @@ -1,18 +1,18 @@  - 999 + 1.0 DXAutomap.AspNet.Data - %meta_description% + DevExtreme data layer extension for ASP.NET supporting Automapper Devexpress_Statler - %meta_project_url% + https://github.com/statler/DevExtreme.AspNet.Data MIT https://secure.gravatar.com/avatar/6b38f1e9ffd8b069bcdc2741934fdbcf?s=512&r=g DXAutomap.AspNet.Data - 999 + 1.0 netstandard2.1 full bin\Debug\$(TargetFramework)\DevExtreme.AspNet.Data.xml From 77a67c86e0387e3d47715cca4033edf60391824d Mon Sep 17 00:00:00 2001 From: "dennis.gascoigne" <337991+statler@users.noreply.github.com> Date: Wed, 12 Apr 2023 09:03:17 +1000 Subject: [PATCH 07/23] Fix to pass parameters to Context.Mapper.ConfigurationProvider.Internal().ProjectionBuilder.GetProjection --- net/DevExtreme.AspNet.Data/DataSourceExpressionBuilder.cs | 7 +++---- net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/net/DevExtreme.AspNet.Data/DataSourceExpressionBuilder.cs b/net/DevExtreme.AspNet.Data/DataSourceExpressionBuilder.cs index 61d5b20f..5a8537c5 100644 --- a/net/DevExtreme.AspNet.Data/DataSourceExpressionBuilder.cs +++ b/net/DevExtreme.AspNet.Data/DataSourceExpressionBuilder.cs @@ -21,15 +21,14 @@ public DataSourceExpressionBuilder(Expression expr, DataSourceLoadContext contex } public Expression BuildLoadExpr(bool paginate, IList filterOverride = null, IReadOnlyList selectOverride = null, Type projectionType = null) { - if (Context.ProjectBeforeFilter) { + if(Context.ProjectBeforeFilter) { AddSelect(selectOverride, projectionType); AddFilter(filterOverride); AddSort(); if(paginate) AddPaging(); return Expr; - } - else { + } else { AddFilter(filterOverride); AddSort(); AddSelect(selectOverride, projectionType); @@ -81,7 +80,7 @@ void AddSelect(IReadOnlyList selectOverride = null, Type projectionType Expr = CreateSelectCompiler().Compile(Expr, selectOverride ?? Context.FullSelect); else if(projectionType != null) { var _type = GetItemType(); - var qryBase = Context.Mapper.ConfigurationProvider.Internal().ProjectionBuilder.GetProjection(_type, projectionType, null, Array.Empty()); + var qryBase = Context.Mapper.ConfigurationProvider.Internal().ProjectionBuilder.GetProjection(_type, projectionType, Context.AutomapperProjectionParameters, Array.Empty()); var qryExpr = qryBase.Projection; Expr = Expression.Call(typeof(Queryable), nameof(Queryable.Select), new[] { _type, qryExpr.ReturnType }, Expr, Expression.Quote(qryExpr)); } diff --git a/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj b/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj index 1c101c8f..fc19ec5d 100644 --- a/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj +++ b/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj @@ -1,7 +1,7 @@  - 1.0 + 1.1 DXAutomap.AspNet.Data DevExtreme data layer extension for ASP.NET supporting Automapper Devexpress_Statler @@ -12,7 +12,7 @@ DXAutomap.AspNet.Data - 1.0 + 1.1 netstandard2.1 full bin\Debug\$(TargetFramework)\DevExtreme.AspNet.Data.xml From 4f334d2923cddcd7fcc70b5129602f60612bd5af Mon Sep 17 00:00:00 2001 From: "dennis.gascoigne" <337991+statler@users.noreply.github.com> Date: Wed, 12 Apr 2023 17:35:14 +1000 Subject: [PATCH 08/23] - Added context (Automapper objects) to customaccessor registration - necessitated changing all ExpressionCompiler derivatives - Injected automapper projections into grouping when ProjectBeforeFilter --- .../Aggregation/AggregateCalculator.cs | 4 +- .../Aggregation/SumFix.cs | 4 +- .../DataSourceExpressionBuilder.cs | 12 ++-- .../DataSourceLoaderImpl.cs | 16 +++-- .../ExpressionCompiler.cs | 16 +++-- .../FilterExpressionCompiler.cs | 3 +- .../Helpers/CustomAccessorCompilers.cs | 65 ++++++++++++++----- .../Helpers/DefaultAccessor.cs | 4 +- .../RemoteGroupExpressionCompiler.cs | 10 +-- .../RemoteGrouping/RemoteGroupTransformer.cs | 7 +- .../SelectExpressionCompiler.cs | 4 +- net/DevExtreme.AspNet.Data/SelectHelper.cs | 4 +- .../SortExpressionCompiler.cs | 4 +- 13 files changed, 100 insertions(+), 53 deletions(-) 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/DataSourceExpressionBuilder.cs b/net/DevExtreme.AspNet.Data/DataSourceExpressionBuilder.cs index 5a8537c5..98fd7825 100644 --- a/net/DevExtreme.AspNet.Data/DataSourceExpressionBuilder.cs +++ b/net/DevExtreme.AspNet.Data/DataSourceExpressionBuilder.cs @@ -44,7 +44,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) @@ -72,7 +75,7 @@ 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, Type projectionType = null) { @@ -99,7 +102,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); } @@ -109,7 +113,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/DataSourceLoaderImpl.cs b/net/DevExtreme.AspNet.Data/DataSourceLoaderImpl.cs index 70b1b9d4..40b2e0b7 100644 --- a/net/DevExtreme.AspNet.Data/DataSourceLoaderImpl.cs +++ b/net/DevExtreme.AspNet.Data/DataSourceLoaderImpl.cs @@ -41,7 +41,9 @@ public DataSourceLoaderImpl(IQueryable source, DataSourceLoadOptionsBase options DataSourceExpressionBuilder CreateBuilder() => new DataSourceExpressionBuilder(Source.Expression, Context); + Type dtoType = null; public async Task LoadAsync() { + dtoType = typeof(TDto) == typeof(S) ? null : typeof(TDto); if(Context.IsCountQuery) return new LoadResult { totalCount = await ExecTotalCountAsync() }; @@ -82,7 +84,6 @@ public async Task LoadAsync() { var deferPaging = Context.HasGroups || !Context.UseRemoteGrouping && !Context.SummaryIsTotalCountOnly && Context.HasSummary; Expression loadExpr; - Type dtoType = typeof(TDto) == typeof(S) ? null : typeof(TDto); if(!deferPaging && Context.PaginateViaPrimaryKey && Context.Take > 0) { if(!Context.HasPrimaryKey) { @@ -133,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; @@ -143,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) @@ -177,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(); } } @@ -205,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/ExpressionCompiler.cs b/net/DevExtreme.AspNet.Data/ExpressionCompiler.cs index 8d62d19c..9f06fe3b 100644 --- a/net/DevExtreme.AspNet.Data/ExpressionCompiler.cs +++ b/net/DevExtreme.AspNet.Data/ExpressionCompiler.cs @@ -12,14 +12,17 @@ namespace DevExtreme.AspNet.Data { 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); } diff --git a/net/DevExtreme.AspNet.Data/FilterExpressionCompiler.cs b/net/DevExtreme.AspNet.Data/FilterExpressionCompiler.cs index 4d120e4c..b478ecae 100644 --- a/net/DevExtreme.AspNet.Data/FilterExpressionCompiler.cs +++ b/net/DevExtreme.AspNet.Data/FilterExpressionCompiler.cs @@ -20,10 +20,9 @@ const string bool _stringToLower; readonly bool _supportsEqualsMethod; - public object RuntimeResolutionContext; public FilterExpressionCompiler(Type itemType, bool guardNulls, bool stringToLower = false, bool supportsEqualsMethod = true, object runtimeResolutionContext = null) - : base(itemType, guardNulls) { + : base(itemType, guardNulls, runtimeResolutionContext) { _stringToLower = stringToLower; RuntimeResolutionContext = runtimeResolutionContext; _supportsEqualsMethod = supportsEqualsMethod; diff --git a/net/DevExtreme.AspNet.Data/Helpers/CustomAccessorCompilers.cs b/net/DevExtreme.AspNet.Data/Helpers/CustomAccessorCompilers.cs index 6b5319e6..44c78774 100644 --- a/net/DevExtreme.AspNet.Data/Helpers/CustomAccessorCompilers.cs +++ b/net/DevExtreme.AspNet.Data/Helpers/CustomAccessorCompilers.cs @@ -11,13 +11,17 @@ 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); } /// @@ -30,6 +34,13 @@ public static void Register(CompilerFunc 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) { @@ -56,22 +67,21 @@ 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 - Register((target, accessorText) => { - var accessor = CustomAccessorLibrary.Get(target, target.Type.Name, accessorText); + RegisterWithContext((target, accessorText, runtimeContext) => { + var accessor = CustomAccessorLibrary.Get(target, target.Type.Name, accessorText, runtimeContext); if(accessor != null) return accessor; return null; }); } - internal static Expression TryCompile(Expression expr, string accessorText) { - if(_compilers.Count < 1) - 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; @@ -79,7 +89,7 @@ internal static Expression TryCompile(Expression expr, string accessorText) { #if DEBUG internal static void Clear() { - _compilers.Clear(); + _compilersWithContext.Clear(); } #endif @@ -98,6 +108,17 @@ public void Add(string TypeName, string PropertyName, LambdaExpression ResolveEx } else _dctAccessors.Add(TypeName, new Dictionary() { [PropertyName] = _accessor }); } + 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 _dctAccessors.Add(TypeName, new Dictionary() { [PropertyName] = _accessor }); + } + 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); } @@ -106,11 +127,11 @@ public void Add(string PropertyName, Expression> ResolveExpress Add(typeof(T).Name, PropertyName, ResolveExpression); } - public Expression Get(Expression target, string TypeName, string PropertyName) { + 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].ResolveExpression; + var expression = expressionForType[PropertyName].GetResolvedExpression(runtimeContext); return new ParameterVisitor(expression.Parameters, target as ParameterExpression) .VisitAndConvert(expression.Body, PropertyName); } @@ -124,13 +145,23 @@ public Expression Get(Expression target, string TypeName, string PropertyName) { public class Accessor { public string TypeName { get; set; } public string PropertyName { get; set; } - public LambdaExpression ResolveExpression { get; set; } + Func ResolveExpression { get; set; } + public Accessor() { } public Accessor(string typeName, string propertyName, LambdaExpression resolveExpression) { TypeName = typeName; PropertyName = propertyName; - ResolveExpression = resolveExpression; + 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); } } 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) { From 76fc3c2c4c8873026138bbef5fe8c3d2c20b6bf3 Mon Sep 17 00:00:00 2001 From: "dennis.gascoigne" <337991+statler@users.noreply.github.com> Date: Wed, 12 Apr 2023 17:37:11 +1000 Subject: [PATCH 09/23] Version bump --- net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj b/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj index fc19ec5d..92dc2a61 100644 --- a/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj +++ b/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj @@ -1,7 +1,7 @@  - 1.1 + 1.11 DXAutomap.AspNet.Data DevExtreme data layer extension for ASP.NET supporting Automapper Devexpress_Statler @@ -12,7 +12,7 @@ DXAutomap.AspNet.Data - 1.1 + 1.11 netstandard2.1 full bin\Debug\$(TargetFramework)\DevExtreme.AspNet.Data.xml From 66774a50bb4dc79dfe26f909f386835bc9ef2824 Mon Sep 17 00:00:00 2001 From: statler <337991+statler@users.noreply.github.com> Date: Wed, 12 Apr 2023 17:51:44 +1000 Subject: [PATCH 10/23] Update README.md --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2c7d3f12..ba53aed6 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ The basic functionality is exactly the same as the devex library. The big differ 2. 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. 3. Specify that filters apply after a projection using the new DataSourceLoadOption property ProjectBeforeFilter 4. Utilise the devex expression builders for binary expressions by calling CompileNonCustomBinary +5. From Version 1.11, CustomAccessors also have access to the tuntime context (Automapper object) ********************************************************************************************************************************************** @@ -40,7 +41,17 @@ In configure, call CustomAccessorCompilers.RegisterAutomapperProfiles. You can a 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; + //}); .... } From 890adba57c80bc49e584696a420b8921552d7e93 Mon Sep 17 00:00:00 2001 From: statler <337991+statler@users.noreply.github.com> Date: Wed, 12 Apr 2023 17:53:00 +1000 Subject: [PATCH 11/23] Update README.md --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ba53aed6..cae723cd 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,11 @@ 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. 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. -3. Specify that filters apply after a projection using the new DataSourceLoadOption property ProjectBeforeFilter -4. Utilise the devex expression builders for binary expressions by calling CompileNonCustomBinary -5. From Version 1.11, CustomAccessors also have access to the tuntime context (Automapper 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) ********************************************************************************************************************************************** From 1980a7b3825aa3a06ce6ce81f8818189dcaf0b1e Mon Sep 17 00:00:00 2001 From: "dennis.gascoigne" <337991+statler@users.noreply.github.com> Date: Wed, 12 Apr 2023 18:54:48 +1000 Subject: [PATCH 12/23] Refactored customfilters to remove redundant collections and fix bug with empty CompilerFuncs --- net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj | 4 ++-- net/DevExtreme.AspNet.Data/FilterExpressionCompiler.cs | 2 +- .../Helpers/CustomFilterCompilers.cs | 8 +------- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj b/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj index 92dc2a61..6108fdb3 100644 --- a/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj +++ b/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj @@ -1,7 +1,7 @@  - 1.11 + 1.12 DXAutomap.AspNet.Data DevExtreme data layer extension for ASP.NET supporting Automapper Devexpress_Statler @@ -12,7 +12,7 @@ DXAutomap.AspNet.Data - 1.11 + 1.12 netstandard2.1 full bin\Debug\$(TargetFramework)\DevExtreme.AspNet.Data.xml diff --git a/net/DevExtreme.AspNet.Data/FilterExpressionCompiler.cs b/net/DevExtreme.AspNet.Data/FilterExpressionCompiler.cs index b478ecae..4df86a16 100644 --- a/net/DevExtreme.AspNet.Data/FilterExpressionCompiler.cs +++ b/net/DevExtreme.AspNet.Data/FilterExpressionCompiler.cs @@ -62,7 +62,7 @@ Expression CompileBinary(ParameterExpression dataItemExpr, IList criteriaJson, b var clientValue = Utils.UnwrapNewtonsoftValue(criteriaJson[hasExplicitOperation ? 2 : 1]); var isStringOperation = clientOperation == CONTAINS || clientOperation == NOT_CONTAINS || clientOperation == STARTS_WITH || clientOperation == ENDS_WITH; - if(shouldProcessCustom && CustomFilterCompilers.Binary.CompilerFuncs.Count > 0) { + if(shouldProcessCustom && CustomFilterCompilers.Binary.CompilerFuncsWithContext.Count > 0) { var customResult = CustomFilterCompilers.Binary.TryCompile(new BinaryExpressionInfo { DataItemExpression = dataItemExpr, AccessorText = clientAccessor, diff --git a/net/DevExtreme.AspNet.Data/Helpers/CustomFilterCompilers.cs b/net/DevExtreme.AspNet.Data/Helpers/CustomFilterCompilers.cs index ad0ac4ec..130e3b75 100644 --- a/net/DevExtreme.AspNet.Data/Helpers/CustomFilterCompilers.cs +++ b/net/DevExtreme.AspNet.Data/Helpers/CustomFilterCompilers.cs @@ -10,15 +10,9 @@ namespace DevExtreme.AspNet.Data.Helpers { 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, FilterExpressionCompiler filterExpressionCompiler) { - foreach(var func in CompilerFuncs) { - var result = func(info); - if(result != null) - return result; - } foreach(var func in CompilerFuncsWithContext) { var result = func(info, filterExpressionCompiler); if(result != null) @@ -29,7 +23,7 @@ internal static Expression TryCompile(IBinaryExpressionInfo info, FilterExpressi } public static void RegisterBinaryExpressionCompiler(BinaryExpressionCompilerFunc compilerFunc) { - Binary.CompilerFuncs.Add(compilerFunc); + Binary.CompilerFuncsWithContext.Add((item, _cx) => compilerFunc(item)); } public static void RegisterBinaryExpressionCompilerWithContext(BinaryExpressionCompilerWithContextFunc compilerFunc) { From c0a6a4b729c9df391d647ed3bea29a44831bcf23 Mon Sep 17 00:00:00 2001 From: statler <337991+statler@users.noreply.github.com> Date: Thu, 13 Apr 2023 21:11:34 +1000 Subject: [PATCH 13/23] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index cae723cd..38f7ce56 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ This is a fork of Devexpress.AspNet.Data, a fantastic expression building librar 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 NuGet\Install-Package DXAutomap.AspNet.Data -Version 1.12.0 + ********************************************************************************************************************************************** How to use: From 2c433f78db9ea5c667b6a01a645732493d592c06 Mon Sep 17 00:00:00 2001 From: statler <337991+statler@users.noreply.github.com> Date: Thu, 13 Apr 2023 21:12:06 +1000 Subject: [PATCH 14/23] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 38f7ce56..dab003f0 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ This is a fork of Devexpress.AspNet.Data, a fantastic expression building librar 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 NuGet\Install-Package DXAutomap.AspNet.Data -Version 1.12.0 +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 ********************************************************************************************************************************************** From 6d7cdb06cbe2a671ed44cee00cd7ae65508bfc24 Mon Sep 17 00:00:00 2001 From: statler <337991+statler@users.noreply.github.com> Date: Thu, 13 Apr 2023 21:12:21 +1000 Subject: [PATCH 15/23] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index dab003f0..356dbe26 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ This is a fork of Devexpress.AspNet.Data, a fantastic expression building librar 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 ********************************************************************************************************************************************** From eda620aef8f1f1ec927b352d48a034ed1d437002 Mon Sep 17 00:00:00 2001 From: "dennis.gascoigne" <337991+statler@users.noreply.github.com> Date: Thu, 21 Mar 2024 16:02:47 +1100 Subject: [PATCH 16/23] Fix for complex multi-projected queries from Automap --- net/.cr/personal/FavoritesList/List.xml | 6 ++++++ .../DataSourceExpressionBuilder.cs | 14 +++++++++----- .../DevExtreme.AspNet.Data.csproj | 5 +++-- 3 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 net/.cr/personal/FavoritesList/List.xml 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/DataSourceExpressionBuilder.cs b/net/DevExtreme.AspNet.Data/DataSourceExpressionBuilder.cs index 98fd7825..c19a752e 100644 --- a/net/DevExtreme.AspNet.Data/DataSourceExpressionBuilder.cs +++ b/net/DevExtreme.AspNet.Data/DataSourceExpressionBuilder.cs @@ -1,13 +1,10 @@ -using AutoMapper; -using AutoMapper.Internal; -using DevExtreme.AspNet.Data.Async; +using AutoMapper.Internal; using DevExtreme.AspNet.Data.RemoteGrouping; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Reflection; namespace DevExtreme.AspNet.Data { @@ -85,7 +82,14 @@ void AddSelect(IReadOnlyList selectOverride = null, Type projectionType var _type = GetItemType(); var qryBase = Context.Mapper.ConfigurationProvider.Internal().ProjectionBuilder.GetProjection(_type, projectionType, Context.AutomapperProjectionParameters, Array.Empty()); var qryExpr = qryBase.Projection; - Expr = Expression.Call(typeof(Queryable), nameof(Queryable.Select), new[] { _type, qryExpr.ReturnType }, Expr, Expression.Quote(qryExpr)); + 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)); + } } } diff --git a/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj b/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj index 6108fdb3..d37af432 100644 --- a/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj +++ b/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj @@ -1,7 +1,7 @@  - 1.12 + 1.14 DXAutomap.AspNet.Data DevExtreme data layer extension for ASP.NET supporting Automapper Devexpress_Statler @@ -12,11 +12,12 @@ DXAutomap.AspNet.Data - 1.12 + 1.14 netstandard2.1 full bin\Debug\$(TargetFramework)\DevExtreme.AspNet.Data.xml 1591 + 1.14.0.0 From 170fa8ed7f283742b433a60cd52c15cc9aaaebad Mon Sep 17 00:00:00 2001 From: "dennis.gascoigne" <337991+statler@users.noreply.github.com> Date: Tue, 11 Jun 2024 09:44:32 +1000 Subject: [PATCH 17/23] Change Dictionary to ConcurrentDictionary --- .../DataSourceExpressionBuilder.cs | 85 ++++++++++++------- .../Helpers/CustomAccessorCompilers.cs | 10 +-- 2 files changed, 57 insertions(+), 38 deletions(-) diff --git a/net/DevExtreme.AspNet.Data/DataSourceExpressionBuilder.cs b/net/DevExtreme.AspNet.Data/DataSourceExpressionBuilder.cs index c19a752e..4911ccc2 100644 --- a/net/DevExtreme.AspNet.Data/DataSourceExpressionBuilder.cs +++ b/net/DevExtreme.AspNet.Data/DataSourceExpressionBuilder.cs @@ -1,58 +1,67 @@ -using AutoMapper.Internal; -using DevExtreme.AspNet.Data.RemoteGrouping; -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -namespace DevExtreme.AspNet.Data { +namespace DevExtreme.AspNet.Data +{ - class DataSourceExpressionBuilder { + class DataSourceExpressionBuilder + { Expression Expr; readonly DataSourceLoadContext Context; - public DataSourceExpressionBuilder(Expression expr, DataSourceLoadContext context) { + public DataSourceExpressionBuilder(Expression expr, DataSourceLoadContext context) + { Expr = expr; Context = context; } - public Expression BuildLoadExpr(bool paginate, IList filterOverride = null, IReadOnlyList selectOverride = null, Type projectionType = null) { - if(Context.ProjectBeforeFilter) { + 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) + if (paginate) AddPaging(); return Expr; - } else { + } + else + { AddFilter(filterOverride); AddSort(); AddSelect(selectOverride, projectionType); - if(paginate) + if (paginate) AddPaging(); return Expr; } } - public Expression BuildCountExpr() { + public Expression BuildCountExpr() + { AddFilter(); AddCount(); return Expr; } - public Expression BuildLoadGroupsExpr(bool paginate, bool suppressGroups = false, bool suppressTotals = false, Type projectionType = null) { - if(Context.ProjectBeforeFilter) { + 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) + if (paginate) AddPaging(); return Expr; } - public Expression BuildGroupCountExpr() { + public Expression BuildGroupCountExpr() + { AddFilter(); Expr = CreateSelectCompiler().CompileSingle(Expr, Context.Group.Single().Selector); Expr = QueryableCall(nameof(Queryable.Distinct)); @@ -60,8 +69,10 @@ public Expression BuildGroupCountExpr() { return Expr; } - void AddFilter(IList filterOverride = null) { - if(filterOverride != null || Context.HasFilter) { + 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, Context.AutomapperProjectionParameters).Compile(filterOverride ?? Context.Filter); @@ -70,38 +81,46 @@ void AddFilter(IList filterOverride = null) { } } - void AddSort() { - if(Context.HasAnySort) + void AddSort() + { + if (Context.HasAnySort) Expr = new SortExpressionCompiler(GetItemType(), Context.GuardNulls, Context.AutomapperProjectionParameters).Compile(Expr, Context.GetFullSort()); } - void AddSelect(IReadOnlyList selectOverride = null, Type projectionType = null) { - if(selectOverride != null || Context.HasAnySelect && Context.UseRemoteSelect) + 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) { + 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) { + 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 { + } + else + { Expr = Expression.Call(typeof(Queryable), nameof(Queryable.Select), new[] { _type, qryExpr.ReturnType }, Expr, Expression.Quote(qryExpr)); } } } - void AddPaging() { - if(Context.Skip > 0) + void AddPaging() + { + if (Context.Skip > 0) Expr = QueryableCall(nameof(Queryable.Skip), Expression.Constant(Context.Skip)); - if(Context.Take > 0) + if (Context.Take > 0) Expr = QueryableCall(nameof(Queryable.Take), Expression.Constant(Context.Take)); } - void AddRemoteGrouping(bool suppressGroups, bool suppressTotals) { + void AddRemoteGrouping(bool suppressGroups, bool suppressTotals) + { var compiler = new RemoteGroupExpressionCompiler( GetItemType(), Context.GuardNulls, Context.ExpandLinqSumType, Context.CreateAnonTypeNewTweaks(), suppressGroups ? null : Context.Group, @@ -112,7 +131,8 @@ void AddRemoteGrouping(bool suppressGroups, bool suppressTotals) { Expr = compiler.Compile(Expr); } - void AddCount() { + void AddCount() + { Expr = QueryableCall(nameof(Queryable.Count)); } @@ -125,11 +145,12 @@ Expression QueryableCall(string methodName) Expression QueryableCall(string methodName, Expression arg) => Expression.Call(typeof(Queryable), methodName, GetQueryableGenericArguments(), Expr, arg); - Type[] GetQueryableGenericArguments() { + Type[] GetQueryableGenericArguments() + { const string queryable1 = "IQueryable`1"; var type = Expr.Type; - if(type.IsInterface && type.Name == queryable1) + if (type.IsInterface && type.Name == queryable1) return type.GenericTypeArguments; return type.GetInterface(queryable1).GenericTypeArguments; diff --git a/net/DevExtreme.AspNet.Data/Helpers/CustomAccessorCompilers.cs b/net/DevExtreme.AspNet.Data/Helpers/CustomAccessorCompilers.cs index 44c78774..7f03f5de 100644 --- a/net/DevExtreme.AspNet.Data/Helpers/CustomAccessorCompilers.cs +++ b/net/DevExtreme.AspNet.Data/Helpers/CustomAccessorCompilers.cs @@ -1,10 +1,9 @@ using AutoMapper; using AutoMapper.Internal; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Linq; using System.Linq.Expressions; namespace DevExtreme.AspNet.Data.Helpers { @@ -96,7 +95,7 @@ internal static void Clear() { } public class AccessorLibrary { - Dictionary> _dctAccessors = new Dictionary>(); + ConcurrentDictionary> _dctAccessors = new ConcurrentDictionary>(); public AccessorLibrary() { } @@ -105,7 +104,7 @@ public void Add(string TypeName, string PropertyName, LambdaExpression ResolveEx if(_dctAccessors.ContainsKey(TypeName)) { var expressionForType = _dctAccessors[TypeName]; expressionForType[PropertyName] = _accessor; - } else _dctAccessors.Add(TypeName, new Dictionary() { [PropertyName] = _accessor }); + } else _dctAccessors.TryAdd(TypeName, new Dictionary() { [PropertyName] = _accessor }); } public void Add(string TypeName, string PropertyName, Func ResolveExprFunc) { @@ -113,7 +112,7 @@ public void Add(string TypeName, string PropertyName, Func() { [PropertyName] = _accessor }); + } else _dctAccessors.TryAdd(TypeName, new Dictionary() { [PropertyName] = _accessor }); } public void Add(string PropertyName, Func ResolveExprFunc) { Add(typeof(T).Name, PropertyName, ResolveExprFunc); @@ -139,7 +138,6 @@ public Expression Get(Expression target, string TypeName, string PropertyName, o return null; } - } public class Accessor { From 9f43fc0c1443d81a87cc8bf5ee4eec0b093c95b2 Mon Sep 17 00:00:00 2001 From: "dennis.gascoigne" <337991+statler@users.noreply.github.com> Date: Tue, 11 Jun 2024 11:20:38 +1000 Subject: [PATCH 18/23] Tests changes --- .../SampleLoadOptions.cs | 5 +--- ...evExtreme.AspNet.Data.Tests.EFCore7.csproj | 3 +- .../DevExtreme.AspNet.Data.Tests.csproj | 2 +- net/DevExtreme.AspNet.Data.sln | 30 ------------------- .../DevExtreme.AspNet.Data.csproj | 4 +-- 5 files changed, 5 insertions(+), 39 deletions(-) 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/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 e544a508..41c931f6 100644 --- a/net/DevExtreme.AspNet.Data.sln +++ b/net/DevExtreme.AspNet.Data.sln @@ -12,16 +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}") = "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.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 @@ -32,26 +22,6 @@ 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 - {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 - {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 diff --git a/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj b/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj index e68fbce7..a546eb26 100644 --- a/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj +++ b/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj @@ -21,9 +21,9 @@ - + - + From 00220d75d996e4eb73f8478026f23e5d02ecfc11 Mon Sep 17 00:00:00 2001 From: "dennis.gascoigne" <337991+statler@users.noreply.github.com> Date: Tue, 18 Jun 2024 09:30:50 +1000 Subject: [PATCH 19/23] Cleanup --- net/DevExtreme.AspNet.Data/DataSourceLoadContext.cs | 2 +- net/DevExtreme.AspNet.Data/QueryProviderInfo.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/net/DevExtreme.AspNet.Data/DataSourceLoadContext.cs b/net/DevExtreme.AspNet.Data/DataSourceLoadContext.cs index 1b1c5985..689fdc27 100644 --- a/net/DevExtreme.AspNet.Data/DataSourceLoadContext.cs +++ b/net/DevExtreme.AspNet.Data/DataSourceLoadContext.cs @@ -48,7 +48,7 @@ public bool RequireQueryableChainBreak { } public AnonTypeNewTweaks CreateAnonTypeNewTweaks() => new AnonTypeNewTweaks { - AllowEmpty = !_providerInfo.IsL2S && !_providerInfo.IsMongoDB, + AllowEmpty = !_providerInfo.IsL2S && !_providerInfo.IsMongoDBB, AllowUnusedMembers = !_providerInfo.IsL2S }; diff --git a/net/DevExtreme.AspNet.Data/QueryProviderInfo.cs b/net/DevExtreme.AspNet.Data/QueryProviderInfo.cs index 1aa740b0..953edd67 100644 --- a/net/DevExtreme.AspNet.Data/QueryProviderInfo.cs +++ b/net/DevExtreme.AspNet.Data/QueryProviderInfo.cs @@ -11,7 +11,7 @@ class QueryProviderInfo { public readonly bool IsXPO; public readonly bool IsNH; public readonly bool IsL2S; - public readonly bool IsMongoDB; + public readonly bool IsMongoDBB; public readonly Version Version; public QueryProviderInfo(IQueryProvider provider) { @@ -33,7 +33,7 @@ public QueryProviderInfo(IQueryProvider provider) { else if(typeName.StartsWith("System.Data.Linq.")) IsL2S = true; else if(typeName.StartsWith("MongoDB.Driver.Linq.")) - IsMongoDB = true; + IsMongoDBB = true; else if(typeName.StartsWith("LinqKit.ExpandableQueryProvider`1") || typeName.StartsWith("LinqKit.ExpandableIncludableQueryProvider`1")) { switch(providerAssembly.GetName().Name) { case "LinqKit.Microsoft.EntityFrameworkCore": From 4d6c4f12857b5902437d9f190d4008eee1071fb0 Mon Sep 17 00:00:00 2001 From: "dennis.gascoigne" <337991+statler@users.noreply.github.com> Date: Tue, 18 Jun 2024 09:36:58 +1000 Subject: [PATCH 20/23] Revert Mogodbb thingo --- net/DevExtreme.AspNet.Data/DataSourceLoadContext.cs | 2 +- net/DevExtreme.AspNet.Data/QueryProviderInfo.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/net/DevExtreme.AspNet.Data/DataSourceLoadContext.cs b/net/DevExtreme.AspNet.Data/DataSourceLoadContext.cs index 689fdc27..1b1c5985 100644 --- a/net/DevExtreme.AspNet.Data/DataSourceLoadContext.cs +++ b/net/DevExtreme.AspNet.Data/DataSourceLoadContext.cs @@ -48,7 +48,7 @@ public bool RequireQueryableChainBreak { } public AnonTypeNewTweaks CreateAnonTypeNewTweaks() => new AnonTypeNewTweaks { - AllowEmpty = !_providerInfo.IsL2S && !_providerInfo.IsMongoDBB, + AllowEmpty = !_providerInfo.IsL2S && !_providerInfo.IsMongoDB, AllowUnusedMembers = !_providerInfo.IsL2S }; diff --git a/net/DevExtreme.AspNet.Data/QueryProviderInfo.cs b/net/DevExtreme.AspNet.Data/QueryProviderInfo.cs index 953edd67..1aa740b0 100644 --- a/net/DevExtreme.AspNet.Data/QueryProviderInfo.cs +++ b/net/DevExtreme.AspNet.Data/QueryProviderInfo.cs @@ -11,7 +11,7 @@ class QueryProviderInfo { public readonly bool IsXPO; public readonly bool IsNH; public readonly bool IsL2S; - public readonly bool IsMongoDBB; + public readonly bool IsMongoDB; public readonly Version Version; public QueryProviderInfo(IQueryProvider provider) { @@ -33,7 +33,7 @@ public QueryProviderInfo(IQueryProvider provider) { else if(typeName.StartsWith("System.Data.Linq.")) IsL2S = true; else if(typeName.StartsWith("MongoDB.Driver.Linq.")) - IsMongoDBB = true; + IsMongoDB = true; else if(typeName.StartsWith("LinqKit.ExpandableQueryProvider`1") || typeName.StartsWith("LinqKit.ExpandableIncludableQueryProvider`1")) { switch(providerAssembly.GetName().Name) { case "LinqKit.Microsoft.EntityFrameworkCore": From 671681d15735b1ab455aa67cee37ab5dc87f3dcd Mon Sep 17 00:00:00 2001 From: "dennis.gascoigne" <337991+statler@users.noreply.github.com> Date: Thu, 4 Jul 2024 09:21:12 +1000 Subject: [PATCH 21/23] Update XPO to 24 --- .../DevExtreme.AspNet.Data.Tests.Xpo.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 @@ - + From 841bb6391e89671c860731ec2cbc2c5fe220c502 Mon Sep 17 00:00:00 2001 From: "dennis.gascoigne" <337991+statler@users.noreply.github.com> Date: Fri, 12 Jul 2024 16:36:10 +1000 Subject: [PATCH 22/23] Update version for nupkg --- net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj b/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj index a546eb26..9b670007 100644 --- a/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj +++ b/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj @@ -1,7 +1,7 @@  - 1.14 + 1.15 DXAutomap.AspNet.Data DevExtreme data layer extension for ASP.NET supporting Automapper Devexpress_Statler From d04cfd828deda4a1ca1ecdaaea0fced9b2ac0a8f Mon Sep 17 00:00:00 2001 From: "dennis.gascoigne" <337991+statler@users.noreply.github.com> Date: Tue, 16 Jul 2024 19:36:47 +1000 Subject: [PATCH 23/23] Catch nested dictionary to prevent concurrency problem --- .../DevExtreme.AspNet.Data.csproj | 6 +++--- .../Helpers/CustomAccessorCompilers.cs | 12 +++++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj b/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj index 9b670007..5f1ae314 100644 --- a/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj +++ b/net/DevExtreme.AspNet.Data/DevExtreme.AspNet.Data.csproj @@ -1,7 +1,7 @@  - 1.15 + 1.16 DXAutomap.AspNet.Data DevExtreme data layer extension for ASP.NET supporting Automapper Devexpress_Statler @@ -12,12 +12,12 @@ DXAutomap.AspNet.Data - 1.15 + 1.16 net7.0 full bin\Debug\$(TargetFramework)\DevExtreme.AspNet.Data.xml 1591 - 1.15.0.0 + 1.16.0.0 diff --git a/net/DevExtreme.AspNet.Data/Helpers/CustomAccessorCompilers.cs b/net/DevExtreme.AspNet.Data/Helpers/CustomAccessorCompilers.cs index 7f03f5de..1e2a2710 100644 --- a/net/DevExtreme.AspNet.Data/Helpers/CustomAccessorCompilers.cs +++ b/net/DevExtreme.AspNet.Data/Helpers/CustomAccessorCompilers.cs @@ -95,7 +95,7 @@ internal static void Clear() { } public class AccessorLibrary { - ConcurrentDictionary> _dctAccessors = new ConcurrentDictionary>(); + ConcurrentDictionary> _dctAccessors = new ConcurrentDictionary>(); public AccessorLibrary() { } @@ -104,7 +104,10 @@ public void Add(string TypeName, string PropertyName, LambdaExpression ResolveEx if(_dctAccessors.ContainsKey(TypeName)) { var expressionForType = _dctAccessors[TypeName]; expressionForType[PropertyName] = _accessor; - } else _dctAccessors.TryAdd(TypeName, new Dictionary() { [PropertyName] = _accessor }); + } else { + var accessorDct = new ConcurrentDictionary() { [PropertyName] = _accessor }; + _dctAccessors.TryAdd(TypeName, accessorDct); + } } public void Add(string TypeName, string PropertyName, Func ResolveExprFunc) { @@ -112,7 +115,10 @@ public void Add(string TypeName, string PropertyName, Func() { [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);