Skip to content

Commit 732d6d9

Browse files
nino-sNino Schoch
and
Nino Schoch
authored
Fix memory issue with expression constants (#93)
* Bump sample project to .NET 8 * Add method to replace constant dynamic values * Bump version to 3.0.2 * Update github workflows to use .NET 8 --------- Co-authored-by: Nino Schoch <nino.schoch@e-vo.at>
1 parent 51ab5b7 commit 732d6d9

File tree

9 files changed

+65
-36
lines changed

9 files changed

+65
-36
lines changed

.github/workflows/main.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
strategy:
1717
matrix:
1818
operating-system: [ubuntu-latest]
19-
dotnet-version: ['6.0.x']
19+
dotnet-version: ['8.0.x']
2020

2121
steps:
2222
- uses: actions/checkout@v4
@@ -26,7 +26,7 @@ jobs:
2626
- name: Setup .NET
2727
uses: actions/setup-dotnet@v4
2828
with:
29-
dotnet-version: '6.0.x'
29+
dotnet-version: '8.0.x'
3030

3131
- name: Build package
3232
run: dotnet build --configuration 'Release'

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
- name: Setup .NET
1818
uses: actions/setup-dotnet@v4
1919
with:
20-
dotnet-version: '6.0.x'
20+
dotnet-version: '8.0.x'
2121

2222
- name: Build packages
2323
run: dotnet build --configuration 'Release'

DataTables.NetStandard.Sample/DataTables.NetStandard.Sample.csproj

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1-
<Project Sdk="Microsoft.NET.Sdk.Web">
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
22

33
<PropertyGroup>
4-
<TargetFramework>net6.0</TargetFramework>
4+
<TargetFramework>net8.0</TargetFramework>
55
<PackageLicenseExpression>MIT</PackageLicenseExpression>
6+
<UserSecretsId>3635bc4d-44f6-4d28-930e-d7d3b2ea765e</UserSecretsId>
67
</PropertyGroup>
78

89
<ItemGroup>
9-
<PackageReference Include="AutoMapper" Version="12.0.1" />
10-
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
10+
<PackageReference Include="AutoMapper" Version="13.0.1" />
1111
<PackageReference Include="DataTables.NetStandard.TemplateMapper" Version="1.0.0" />
12-
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.16" />
13-
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer.Design" Version="1.1.6" />
12+
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.3">
13+
<PrivateAssets>all</PrivateAssets>
14+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
15+
</PackageReference>
16+
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.3" />
1417
</ItemGroup>
1518

1619
<ItemGroup>

DataTables.NetStandard.Sample/DataTables/PersonDataTable.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ public override IList<DataTablesColumn<Person, PersonViewModel>> Columns()
2828
PrivatePropertyName = nameof(Person.Id),
2929
IsOrderable = true,
3030
IsSearchable = true,
31-
SearchPredicate = (p, s) => false, // The fallback predicate will never match, but since we declared a provider, it is not used anyway.
32-
SearchPredicateProvider = (s) => (p, s) => true, // The provider will return a predicate matching all entities (used for global search). This is just for illustration, it makes no sense.
33-
ColumnSearchPredicateProvider = (s) => // The column provider will return a predicate matching entities in a numeric range if the search term is properly formatted.
31+
SearchPredicate = (p, s) => false, // The fallback predicate will never match, but since we declared a provider, it is not used anyway.
32+
//SearchPredicateProvider = (s) => (p, s) => true, // The provider will return a predicate matching all entities (used for global search). This is just for illustration, it makes no sense.
33+
ColumnSearchPredicateProvider = (s) => // The column provider will return a predicate matching entities in a numeric range if the search term is properly formatted.
3434
{
3535
var minMax = s.Split("-delim-", System.StringSplitOptions.RemoveEmptyEntries);
3636
if (minMax.Length >= 2)
Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,4 @@
1-
{
2-
"iisSettings": {
3-
"windowsAuthentication": false,
4-
"anonymousAuthentication": true,
5-
"iisExpress": {
6-
"applicationUrl": "http://localhost:53215",
7-
"sslPort": 44331
8-
}
9-
},
1+
{
102
"profiles": {
113
"IIS Express": {
124
"commandName": "IISExpress",
@@ -18,10 +10,28 @@
1810
"DataTables.NetStandard.Sample": {
1911
"commandName": "Project",
2012
"launchBrowser": true,
21-
"applicationUrl": "https://localhost:5001;http://localhost:5000",
2213
"environmentVariables": {
2314
"ASPNETCORE_ENVIRONMENT": "Development"
24-
}
15+
},
16+
"applicationUrl": "https://localhost:5003;http://localhost:5002"
17+
},
18+
"WSL": {
19+
"commandName": "WSL2",
20+
"launchBrowser": true,
21+
"launchUrl": "https://localhost:5001",
22+
"environmentVariables": {
23+
"ASPNETCORE_ENVIRONMENT": "Development",
24+
"ASPNETCORE_URLS": "https://localhost:5003;http://localhost:5002"
25+
},
26+
"distributionName": ""
27+
}
28+
},
29+
"iisSettings": {
30+
"windowsAuthentication": false,
31+
"anonymousAuthentication": true,
32+
"iisExpress": {
33+
"applicationUrl": "http://localhost:53215",
34+
"sslPort": 44331
2535
}
2636
}
2737
}

DataTables.NetStandard/DataTables.NetStandard.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
<TargetFramework>netstandard2.1</TargetFramework>
55
<LangVersion>latest</LangVersion>
66
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
7-
<Version>3.0.1</Version>
8-
<AssemblyVersion>3.0.1.0</AssemblyVersion>
9-
<FileVersion>3.0.1.0</FileVersion>
7+
<Version>3.0.2</Version>
8+
<AssemblyVersion>3.0.2.0</AssemblyVersion>
9+
<FileVersion>3.0.2.0</FileVersion>
1010
<Authors>Namoshek (Marvin Mall)</Authors>
1111
<Company>Namoshek (Marvin Mall)</Company>
1212
<PackageId>DataTables.NetStandard</PackageId>

DataTables.NetStandard/DataTablesRequest.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,13 @@ public class DataTablesRequest<TEntity, TEntityViewModel>
5151

5252
/// <summary>
5353
/// Collection of DataTables column info.
54-
/// Each column can be acessed via indexer by corresponding property name or by property selector.
54+
/// Each column can be acessed via indexer by corresponding property name or by property selector.
5555
/// </summary>
5656
/// <example>
5757
/// Example for an entity Student that has public property FirstName.
5858
/// <code>
5959
/// // Get DataTables request from Http query parameters
60-
/// var request = new DataTablesRequest&lt;Student&gt;(url);
60+
/// var request = new DataTablesRequest&lt;Student&gt;(url);
6161
///
6262
/// // Access by property name
6363
/// var column = request.Columns["FirstName"];
@@ -70,7 +70,7 @@ public class DataTablesRequest<TEntity, TEntityViewModel>
7070
new List<DataTablesColumn<TEntity, TEntityViewModel>>();
7171

7272
/// <summary>
73-
/// Set this property to log incoming request parameters and resulting queries to the given delegate.
73+
/// Set this property to log incoming request parameters and resulting queries to the given delegate.
7474
/// For example, to log to the console, set this property to <see cref="Console.Write(string)"/>.
7575
/// </summary>
7676
public Action<string> Log { get; set; }
@@ -194,7 +194,7 @@ protected void ParseGlobalConfigurationFromQuery(NameValueCollection query)
194194
{
195195
int start = int.TryParse(query["start"], out start) ? start : 0;
196196
PageSize = int.TryParse(query["length"], out int length) ? length : 15;
197-
PageNumber = start / PageSize + 1;
197+
PageNumber = (start / PageSize) + 1;
198198

199199
Draw = int.TryParse(query["draw"], out int draw) ? draw : 0;
200200

DataTables.NetStandard/Extensions/QueryableExtensions.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ internal static IQueryable<TEntity> ApplyGlobalSearchFilter<TEntity, TEntityView
200200
{
201201
var expr = searchPredicate;
202202
var entityParam = ExpressionHelper.BuildParameterExpression<TEntity>();
203-
var searchValueConstant = Expression.Constant(globalSearchValue, typeof(string));
203+
var searchValueConstant = ExpressionHelper.CreateConstantFilterExpression(globalSearchValue, typeof(string));
204204
expression = (Expression<Func<TEntity, bool>>)Expression.Lambda(
205205
Expression.Invoke(expr, entityParam, searchValueConstant),
206206
entityParam);
@@ -233,7 +233,7 @@ internal static IQueryable<TEntity> ApplyGlobalSearchFilter<TEntity, TEntityView
233233
}
234234

235235
/// <summary>
236-
/// Applies the search filter for each of the searchable <see cref="DataTablesRequest{TEntity, TEntityViewModel}.Columns"/>
236+
/// Applies the search filter for each of the searchable <see cref="DataTablesRequest{TEntity, TEntityViewModel}.Columns"/>
237237
/// where a search value is present.
238238
/// </summary>
239239
/// <typeparam name="TEntity">The type of the entity.</typeparam>
@@ -272,7 +272,7 @@ internal static IQueryable<TEntity> ApplyColumnSearchFilter<TEntity, TEntityView
272272
{
273273
var expr = searchPredicate;
274274
var entityParam = ExpressionHelper.BuildParameterExpression<TEntity>();
275-
var searchValueConstant = Expression.Constant(c.SearchValue, typeof(string));
275+
var searchValueConstant = ExpressionHelper.CreateConstantFilterExpression(c.SearchValue, typeof(string));
276276
expression = (Expression<Func<TEntity, bool>>)Expression.Lambda(
277277
Expression.Invoke(expr, entityParam, searchValueConstant),
278278
entityParam);

DataTables.NetStandard/Util/ExpressionHelper.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ internal static ParameterExpression BuildParameterExpression<TEntity>()
3838

3939
/// <summary>
4040
/// Builds an <see cref="Expression"/> for the given <paramref name="propertyName"/>.
41-
/// The property name has to be given as dot-separated property path, e.g. <code>Destination.Location.City</code>.
41+
/// The property name has to be given as dot-separated property path, e.g. <c>Destination.Location.City</c>.
4242
/// </summary>
4343
/// <param name="param">The parameter.</param>
4444
/// <param name="propertyName">Name of the property.</param>
@@ -85,7 +85,7 @@ internal static Expression<Func<TEntity, bool>> BuildStringContainsPredicate<TEn
8585
stringConstant = stringConstant.ToLower();
8686
}
8787

88-
var someValue = Expression.Constant(stringConstant, typeof(string));
88+
var someValue = CreateConstantFilterExpression(stringConstant, typeof(string));
8989
var containsMethodExp = Expression.Call(exp, String_Contains, someValue);
9090

9191
var notNullExp = Expression.NotEqual(exp, Expression.Constant(null, typeof(object)));
@@ -112,12 +112,28 @@ internal static Expression<Func<TEntity, bool>> BuildRegexPredicate<TEntity>(str
112112
exp = Expression.Call(propertyExp, Object_ToString);
113113
}
114114

115-
var regexExp = Expression.Constant(regex, typeof(string));
115+
var regexExp = CreateConstantFilterExpression(regex, typeof(string));
116116
var resultExp = Expression.Call(Regex_IsMatch, exp, regexExp);
117117

118118
var notNullExp = Expression.NotEqual(exp, Expression.Constant(null, typeof(object)));
119119

120120
return Expression.Lambda<Func<TEntity, bool>>(Expression.AndAlso(notNullExp, resultExp), parameterExp);
121121
}
122+
123+
/// <summary>
124+
/// Creates a constant filter expression of the given <paramref name="value"/> and converts the type to the given <paramref name="type"/>.
125+
/// </summary>
126+
/// <param name="value"></param>
127+
/// <param name="type"></param>
128+
internal static Expression CreateConstantFilterExpression(object value, Type type)
129+
{
130+
// The value is converted to anonymous function only returning the value itself.
131+
Expression<Func<object>> valueExpression = () => value;
132+
133+
// Afterwards only the body of the function, which is the value, is converted to the delivered type.
134+
// Therefore no Expression.Constant is necessary which lead to memory leaks, because EFCore caches such constants.
135+
// Caching constants is not wrong, but creating constants of dynamic search values is wrong.
136+
return Expression.Convert(valueExpression.Body, type);
137+
}
122138
}
123139
}

0 commit comments

Comments
 (0)