Skip to content

Commit

Permalink
Fix memory issue with expression constants (#133)
Browse files Browse the repository at this point in the history
* Bump sample project to .NET 8

* Add method to replace constant dynamic values

* Bump version to 3.0.1

* Update github workflows to use .NET 8

---------

Co-authored-by: Nino Schoch <[email protected]>
  • Loading branch information
nino-s and Nino Schoch authored Mar 20, 2024
1 parent 49bf04d commit cc7f8f6
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 53 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
strategy:
matrix:
operating-system: [ubuntu-latest]
dotnet-version: ['6.0.x']
dotnet-version: ['8.0.x']

steps:
- uses: actions/checkout@v4
Expand All @@ -26,7 +26,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '6.0.x'
dotnet-version: '8.0.x'

- name: Build package
run: dotnet build --configuration 'Release'
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '6.0.x'
dotnet-version: '8.0.x'

- name: Build packages
run: dotnet build --configuration 'Release'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<UserSecretsId>b2f560c7-48f9-465c-a1f0-501854a09390</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="DataTables.NetStandard.TemplateMapper" Version="1.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.16">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.16" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.3" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"DataTables.NetStandard.Enhanced.Sample": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"applicationUrl": "https://localhost:5004;http://localhost:5003",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<LangVersion>latest</LangVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Version>3.0.0</Version>
<AssemblyVersion>3.0.0.0</AssemblyVersion>
<FileVersion>3.0.0.0</FileVersion>
<Version>3.0.1</Version>
<AssemblyVersion>3.0.1.0</AssemblyVersion>
<FileVersion>3.0.1.0</FileVersion>
<Authors>Namoshek (Marvin Mall)</Authors>
<Company>Namoshek (Marvin Mall)</Company>
<PackageId>DataTables.NetStandard.Enhanced</PackageId>
Expand All @@ -21,7 +21,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="DataTables.NetStandard" Version="3.0.1" />
<PackageReference Include="DataTables.NetStandard" Version="3.0.2" />
<PackageReference Include="morelinq" Version="4.1.0" />
</ItemGroup>

Expand Down
78 changes: 40 additions & 38 deletions DataTables.NetStandard.Enhanced/EnhancedDataTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public abstract class EnhancedDataTable<TEntity, TEntityViewModel> : DataTable<T
{
protected DataTablesFilterConfiguration _filterConfiguration;

public EnhancedDataTable()
protected EnhancedDataTable()
{
_filterConfiguration = new DataTablesFilterConfiguration();
}
Expand Down Expand Up @@ -104,7 +104,7 @@ public virtual TextInputFilter CreateTextInputFilter(Action<TextInputFilter> con
/// <param name="keyValueSelector"></param>
/// <param name="configure"></param>
/// <returns></returns>
public virtual SelectFilter<TEntity> CreateSelectFilter(Expression<Func<TEntity, LabelValuePair>> keyValueSelector,
public virtual SelectFilter<TEntity> CreateSelectFilter(Expression<Func<TEntity, LabelValuePair>> keyValueSelector,
Action<SelectFilter<TEntity>> configure = null)
{
var filter = new SelectFilter<TEntity>(keyValueSelector)
Expand All @@ -126,7 +126,7 @@ public virtual SelectFilter<TEntity> CreateSelectFilter(Expression<Func<TEntity,
/// <param name="filterOptions"></param>
/// <param name="configure"></param>
/// <returns></returns>
public virtual SelectFilter<TEntity> CreateSelectFilter(IList<LabelValuePair> filterOptions,
public virtual SelectFilter<TEntity> CreateSelectFilter(IList<LabelValuePair> filterOptions,
Action<SelectFilter<TEntity>> configure = null)
{
var filter = new SelectFilter<TEntity>(filterOptions)
Expand All @@ -144,7 +144,8 @@ public virtual SelectFilter<TEntity> CreateSelectFilter(IList<LabelValuePair> fi
/// <summary>
/// Returns a list of distinct column values that can be used for select filters.
/// </summary>
/// <param name="property"></param>
/// <param name="selector"></param>
/// <param name="request"></param>
public virtual IList<LabelValuePair> GetDistinctColumnValuesForSelect(Expression<Func<TEntity, LabelValuePair>> selector,
DataTablesRequest<TEntity, TEntityViewModel> request)
{
Expand All @@ -161,7 +162,8 @@ public virtual IList<LabelValuePair> GetDistinctColumnValuesForSelect(Expression
.Select(g => g.First())
.ToList();
}


/// <summary>
/// Creates a new multi select filter based on the default configuration and the given key value selector.
/// Can be further configured by the given <paramref name="configure"/> action.
/// </summary>
Expand Down Expand Up @@ -331,7 +333,7 @@ protected virtual Expression<Func<TEntity, string, bool>> BuildMultiSelectSearch
var entityParam = propertySelector.Parameters.First();
var searchTermParam = Expression.Parameter(typeof(string));

var itemsConst = Expression.Constant(items, typeof(List<string>));
var itemsConst = ExpressionHelper.CreateConstantFilterExpression(items, typeof(List<string>));

var containsMethod = typeof(List<string>).GetMethod(nameof(List<string>.Contains), new Type[] { typeof(string) });

Expand Down Expand Up @@ -419,10 +421,10 @@ protected virtual Expression<Func<TEntity, string, bool>> BuildNumericRangeSearc
var entityParam = propertySelector.Parameters.First();
var searchTermParam = Expression.Parameter(typeof(string));

var nullableMinConst = Expression.Constant(min, typeof(long?));
var nullableMaxConst = Expression.Constant(max, typeof(long?));
var minConst = Expression.Constant(min ?? 0, typeof(long));
var maxConst = Expression.Constant(max ?? long.MaxValue, typeof(long));
var nullableMinConst = ExpressionHelper.CreateConstantFilterExpression(min, typeof(long?));
var nullableMaxConst = ExpressionHelper.CreateConstantFilterExpression(max, typeof(long?));
var minConst = ExpressionHelper.CreateConstantFilterExpression(min ?? 0, typeof(long));
var maxConst = ExpressionHelper.CreateConstantFilterExpression(max ?? long.MaxValue, typeof(long));
var nullConst = Expression.Constant(null, typeof(long?));

return Expression.Lambda<Func<TEntity, string, bool>>(
Expand Down Expand Up @@ -514,10 +516,10 @@ protected virtual Expression<Func<TEntity, string, bool>> BuildNumericRangeSearc
var entityParam = propertySelector.Parameters.First();
var searchTermParam = Expression.Parameter(typeof(string));

var nullableMinConst = Expression.Constant(min, typeof(int?));
var nullableMaxConst = Expression.Constant(max, typeof(int?));
var minConst = Expression.Constant(min ?? 0, typeof(int));
var maxConst = Expression.Constant(max ?? int.MaxValue, typeof(int));
var nullableMinConst = ExpressionHelper.CreateConstantFilterExpression(min, typeof(int?));
var nullableMaxConst = ExpressionHelper.CreateConstantFilterExpression(max, typeof(int?));
var minConst = ExpressionHelper.CreateConstantFilterExpression(min ?? 0, typeof(int));
var maxConst = ExpressionHelper.CreateConstantFilterExpression(max ?? int.MaxValue, typeof(int));
var nullConst = Expression.Constant(null, typeof(int?));

return Expression.Lambda<Func<TEntity, string, bool>>(
Expand All @@ -543,7 +545,7 @@ protected virtual Expression<Func<TEntity, string, bool>> BuildNumericRangeSearc
/// <param name="delimiter"></param>
/// <returns></returns>
protected virtual Func<string, Expression<Func<TEntity, string, bool>>> CreateNumericRangeSearchPredicateProvider(
Expression<Func<TEntity, long?>> propertySelector,
Expression<Func<TEntity, long?>> propertySelector,
string delimiter = "-")
{
return (s) =>
Expand Down Expand Up @@ -609,8 +611,8 @@ protected virtual Expression<Func<TEntity, string, bool>> BuildNumericRangeSearc
var entityParam = propertySelector.Parameters.First();
var searchTermParam = Expression.Parameter(typeof(string));

var nullableMinConst = Expression.Constant(min, typeof(long?));
var nullableMaxConst = Expression.Constant(max, typeof(long?));
var nullableMinConst = ExpressionHelper.CreateConstantFilterExpression(min, typeof(long?));
var nullableMaxConst = ExpressionHelper.CreateConstantFilterExpression(max, typeof(long?));
var nullConst = Expression.Constant(null, typeof(long?));

return Expression.Lambda<Func<TEntity, string, bool>>(
Expand All @@ -636,7 +638,7 @@ protected virtual Expression<Func<TEntity, string, bool>> BuildNumericRangeSearc
/// <param name="delimiter"></param>
/// <returns></returns>
protected virtual Func<string, Expression<Func<TEntity, string, bool>>> CreateNumericRangeSearchPredicateProvider(
Expression<Func<TEntity, int?>> propertySelector,
Expression<Func<TEntity, int?>> propertySelector,
string delimiter = "-")
{
return (s) =>
Expand Down Expand Up @@ -695,15 +697,15 @@ protected virtual Func<string, Expression<Func<TEntity, string, bool>>> CreateNu
/// <param name="max"></param>
/// <returns></returns>
protected virtual Expression<Func<TEntity, string, bool>> BuildNumericRangeSearchExpression(
Expression<Func<TEntity, int?>> propertySelector,
int? min,
Expression<Func<TEntity, int?>> propertySelector,
int? min,
int? max)
{
var entityParam = propertySelector.Parameters.First();
var searchTermParam = Expression.Parameter(typeof(string));

var nullableMinConst = Expression.Constant(min, typeof(int?));
var nullableMaxConst = Expression.Constant(max, typeof(int?));
var nullableMinConst = ExpressionHelper.CreateConstantFilterExpression(min, typeof(int?));
var nullableMaxConst = ExpressionHelper.CreateConstantFilterExpression(max, typeof(int?));
var nullConst = Expression.Constant(null, typeof(int?));

return Expression.Lambda<Func<TEntity, string, bool>>(
Expand Down Expand Up @@ -732,7 +734,7 @@ protected virtual Expression<Func<TEntity, string, bool>> BuildNumericRangeSearc
/// <param name="dateParseFunction"></param>
/// <returns></returns>
protected virtual Func<string, Expression<Func<TEntity, string, bool>>> CreateDateRangeSearchPredicateProvider(
Expression<Func<TEntity, DateTimeOffset>> propertySelector,
Expression<Func<TEntity, DateTimeOffset>> propertySelector,
string delimiter = "~",
Func<string, DateTimeOffset?> dateParseFunction = null)
{
Expand Down Expand Up @@ -808,17 +810,17 @@ protected virtual Func<string, Expression<Func<TEntity, string, bool>>> CreateDa
/// <param name="max"></param>
/// <returns></returns>
protected virtual Expression<Func<TEntity, string, bool>> BuilDateRangeSearchExpression(
Expression<Func<TEntity, DateTimeOffset>> propertySelector,
DateTimeOffset? min,
Expression<Func<TEntity, DateTimeOffset>> propertySelector,
DateTimeOffset? min,
DateTimeOffset? max)
{
var entityParam = propertySelector.Parameters.First();
var searchTermParam = Expression.Parameter(typeof(string));

var nullableMinConst = Expression.Constant(min, typeof(DateTimeOffset?));
var nullableMaxConst = Expression.Constant(max, typeof(DateTimeOffset?));
var minConst = Expression.Constant(min ?? DateTimeOffset.MinValue, typeof(DateTimeOffset));
var maxConst = Expression.Constant(max ?? DateTimeOffset.MaxValue, typeof(DateTimeOffset));
var nullableMinConst = ExpressionHelper.CreateConstantFilterExpression(min, typeof(DateTimeOffset?));
var nullableMaxConst = ExpressionHelper.CreateConstantFilterExpression(max, typeof(DateTimeOffset?));
var minConst = ExpressionHelper.CreateConstantFilterExpression(min ?? DateTimeOffset.MinValue, typeof(DateTimeOffset));
var maxConst = ExpressionHelper.CreateConstantFilterExpression(max ?? DateTimeOffset.MaxValue, typeof(DateTimeOffset));
var nullConst = Expression.Constant(null, typeof(DateTimeOffset?));

if (min.HasValue && max.HasValue && min == max)
Expand Down Expand Up @@ -855,7 +857,7 @@ protected virtual Expression<Func<TEntity, string, bool>> BuilDateRangeSearchExp
/// <param name="dateParseFunction"></param>
/// <returns></returns>
protected virtual Func<string, Expression<Func<TEntity, string, bool>>> CreateDateRangeSearchPredicateProvider(
Expression<Func<TEntity, DateTime>> propertySelector,
Expression<Func<TEntity, DateTime>> propertySelector,
string delimiter = "~",
Func<string, DateTime?> dateParseFunction = null)
{
Expand Down Expand Up @@ -938,10 +940,10 @@ protected virtual Expression<Func<TEntity, string, bool>> BuilDateRangeSearchExp
var entityParam = propertySelector.Parameters.First();
var searchTermParam = Expression.Parameter(typeof(string));

var nullableMinConst = Expression.Constant(min, typeof(DateTime?));
var nullableMaxConst = Expression.Constant(max, typeof(DateTime?));
var minConst = Expression.Constant(min ?? DateTime.MinValue, typeof(DateTime));
var maxConst = Expression.Constant(max ?? DateTime.MaxValue, typeof(DateTime));
var nullableMinConst = ExpressionHelper.CreateConstantFilterExpression(min, typeof(DateTime?));
var nullableMaxConst = ExpressionHelper.CreateConstantFilterExpression(max, typeof(DateTime?));
var minConst = ExpressionHelper.CreateConstantFilterExpression(min ?? DateTime.MinValue, typeof(DateTime));
var maxConst = ExpressionHelper.CreateConstantFilterExpression(max ?? DateTime.MaxValue, typeof(DateTime));
var nullConst = Expression.Constant(null, typeof(DateTime?));

if (min.HasValue && max.HasValue && min == max)
Expand Down Expand Up @@ -1061,8 +1063,8 @@ protected virtual Expression<Func<TEntity, string, bool>> BuilDateRangeSearchExp
var entityParam = propertySelector.Parameters.First();
var searchTermParam = Expression.Parameter(typeof(string));

var nullableMinConst = Expression.Constant(min, typeof(DateTimeOffset?));
var nullableMaxConst = Expression.Constant(max, typeof(DateTimeOffset?));
var nullableMinConst = ExpressionHelper.CreateConstantFilterExpression(min, typeof(DateTimeOffset?));
var nullableMaxConst = ExpressionHelper.CreateConstantFilterExpression(max, typeof(DateTimeOffset?));
var nullConst = Expression.Constant(null, typeof(DateTimeOffset?));

if (min.HasValue && max.HasValue && min == max)
Expand Down Expand Up @@ -1182,8 +1184,8 @@ protected virtual Expression<Func<TEntity, string, bool>> BuilDateRangeSearchExp
var entityParam = propertySelector.Parameters.First();
var searchTermParam = Expression.Parameter(typeof(string));

var nullableMinConst = Expression.Constant(min, typeof(DateTime?));
var nullableMaxConst = Expression.Constant(max, typeof(DateTime?));
var nullableMinConst = ExpressionHelper.CreateConstantFilterExpression(min, typeof(DateTime?));
var nullableMaxConst = ExpressionHelper.CreateConstantFilterExpression(max, typeof(DateTime?));
var nullConst = Expression.Constant(null, typeof(DateTime?));

if (min.HasValue && max.HasValue && min == max)
Expand Down
24 changes: 24 additions & 0 deletions DataTables.NetStandard.Enhanced/Util/ExpressionHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using System.Linq.Expressions;

namespace DataTables.NetStandard.Enhanced.Util
{
public static class ExpressionHelper
{
/// <summary>
/// Creates a constant filter expression of the given <paramref name="value"/> and converts the type to the given <paramref name="type"/>.
/// </summary>
/// <param name="value"></param>
/// <param name="type"></param>
internal static Expression CreateConstantFilterExpression(object value, Type type)
{
// The value is converted to anonymous function only returning the value itself.
Expression<Func<object>> valueExpression = () => value;

// Afterwards only the body of the function, which is the value, is converted to the delivered type.
// Therefore no Expression.Constant is necessary which lead to memory leaks, because EFCore caches such constants.
// Caching constants is not wrong, but creating constants of dynamic search values is wrong.
return Expression.Convert(valueExpression.Body, type);
}
}
}

0 comments on commit cc7f8f6

Please sign in to comment.