diff --git a/Sieve/Models/SieveOptions.cs b/Sieve/Models/SieveOptions.cs index af0ee0b..4c19575 100644 --- a/Sieve/Models/SieveOptions.cs +++ b/Sieve/Models/SieveOptions.cs @@ -1,4 +1,4 @@ -namespace Sieve.Models +namespace Sieve.Models { public class SieveOptions { @@ -13,5 +13,7 @@ public class SieveOptions public bool IgnoreNullsOnNotEqual { get; set; } = true; public bool DisableNullableTypeExpressionForSorting { get; set; } = false; + + public string CultureNameOfTypeConversion { get; set; } = "en"; } } diff --git a/Sieve/Services/SieveProcessor.cs b/Sieve/Services/SieveProcessor.cs index 5b36ea1..b586b51 100644 --- a/Sieve/Services/SieveProcessor.cs +++ b/Sieve/Services/SieveProcessor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Globalization; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -172,6 +173,8 @@ protected virtual IQueryable ApplyFiltering(TSieveModel model, return result; } + var cultureInfoToUseForTypeConversion = new CultureInfo(Options.Value.CultureNameOfTypeConversion ?? "en"); + Expression outerExpression = null; var parameter = Expression.Parameter(typeof(TEntity), "e"); foreach (var filterTerm in model.GetFiltersParsed()) @@ -198,7 +201,7 @@ protected virtual IQueryable ApplyFiltering(TSieveModel model, var filterValue = isFilterTermValueNull ? Expression.Constant(null, property.PropertyType) - : ConvertStringValueToConstantExpression(filterTermValue, property, converter); + : ConvertStringValueToConstantExpression(filterTermValue, property, converter, cultureInfoToUseForTypeConversion); if (filterTerm.OperatorIsCaseInsensitive && !isFilterTermValueNull) { @@ -309,15 +312,14 @@ private static Expression GenerateFilterNullCheckExpression(Expression propertyV Expression.NotEqual(propertyValue, Expression.Default(propertyValue.Type))); } - private static Expression ConvertStringValueToConstantExpression(string value, PropertyInfo property, - TypeConverter converter) + private static Expression ConvertStringValueToConstantExpression(string value, PropertyInfo property, TypeConverter converter, CultureInfo cultureInfo) { // to allow user to distinguish between prop==null (as null) and prop==\null (as "null"-string) value = value.Equals(EscapeChar + NullFilterValue, StringComparison.InvariantCultureIgnoreCase) ? value.TrimStart(EscapeChar) : value; dynamic constantVal = converter.CanConvertFrom(typeof(string)) - ? converter.ConvertFrom(value) + ? converter.ConvertFrom(null, cultureInfo, value) : Convert.ChangeType(value, property.PropertyType); return GetClosureOverConstant(constantVal, property.PropertyType); diff --git a/SieveUnitTests/Abstractions/Entity/IPost.cs b/SieveUnitTests/Abstractions/Entity/IPost.cs index b05d365..716f91c 100644 --- a/SieveUnitTests/Abstractions/Entity/IPost.cs +++ b/SieveUnitTests/Abstractions/Entity/IPost.cs @@ -9,6 +9,8 @@ public interface IPost: IBaseEntity string Title { get; set; } [Sieve(CanFilter = true, CanSort = true)] int LikeCount { get; set; } + [Sieve(CanFilter = true)] + float MeanRating { get; set; } [Sieve(CanFilter = true, CanSort = true)] int CommentCount { get; set; } [Sieve(CanFilter = true, CanSort = true)] diff --git a/SieveUnitTests/Entities/Post.cs b/SieveUnitTests/Entities/Post.cs index aa931e9..d0ba59f 100644 --- a/SieveUnitTests/Entities/Post.cs +++ b/SieveUnitTests/Entities/Post.cs @@ -12,6 +12,9 @@ public class Post : BaseEntity, IPost [Sieve(CanFilter = true, CanSort = true)] public int LikeCount { get; set; } = new Random().Next(0, 1000); + + [Sieve(CanFilter = true)] + public float MeanRating { get; set; } [Sieve(CanFilter = true, CanSort = true)] public int CommentCount { get; set; } = new Random().Next(0, 1000); diff --git a/SieveUnitTests/General.cs b/SieveUnitTests/General.cs index 886ce50..9b8f780 100644 --- a/SieveUnitTests/General.cs +++ b/SieveUnitTests/General.cs @@ -40,6 +40,7 @@ public General(ITestOutputHelper testOutputHelper) Id = 0, Title = "A", LikeCount = 100, + MeanRating = 3.5f, IsDraft = true, CategoryId = null, TopComment = new Comment { Id = 0, Text = "A1" }, @@ -50,6 +51,7 @@ public General(ITestOutputHelper testOutputHelper) Id = 1, Title = "B", LikeCount = 50, + MeanRating = 3.5f, IsDraft = false, CategoryId = 1, TopComment = new Comment { Id = 3, Text = "B1" }, @@ -60,6 +62,7 @@ public General(ITestOutputHelper testOutputHelper) Id = 2, Title = "C", LikeCount = 0, + MeanRating = 3.5f, CategoryId = 1, TopComment = new Comment { Id = 2, Text = "C1" }, FeaturedComment = new Comment { Id = 6, Text = "C2" } @@ -69,6 +72,7 @@ public General(ITestOutputHelper testOutputHelper) Id = 3, Title = "D", LikeCount = 3, + MeanRating = 3.5f, IsDraft = true, CategoryId = 2, TopComment = new Comment { Id = 1, Text = "D1" }, @@ -79,6 +83,7 @@ public General(ITestOutputHelper testOutputHelper) Id = 4, Title = "Yen", LikeCount = 5, + MeanRating = 3.5f, IsDraft = true, CategoryId = 5, TopComment = new Comment { Id = 4, Text = "Yen3" }, @@ -848,5 +853,76 @@ public void CanFilterWithEscapedOperators(string filter) Assert.Equal(1, resultCount); } + [Theory] + [InlineData("en-US", "1.2")] + [InlineData("de-DE", @"1\,2")] + public void ParseFloatsWithChangedCultureInfo_Works(string culture, string value) + { + // ARRANGE + var posts = new List + { + new Post + { + Id = 1, + Title = "E\\", + LikeCount = 4, + MeanRating = 1.2f, + IsDraft = true, + CategoryId = 1, + TopComment = new Comment { Id = 1, Text = "E1" }, + FeaturedComment = new Comment { Id = 7, Text = "E2" } + } + }.AsQueryable(); + + var optionsAccessor = new SieveOptionsAccessor(); + optionsAccessor.Value.CultureNameOfTypeConversion = culture; + + var processor = new ApplicationSieveProcessor(optionsAccessor, + new SieveCustomSortMethods(), + new SieveCustomFilterMethods()); + + var model = new SieveModel() { Filters = $"MeanRating=={value}" }; + + // ACT + var result = processor.Apply(model, posts); + + // ASSERT + Assert.NotNull(result); + } + + [Theory] + [InlineData("en", @"1\,2")] + [InlineData("de", "1.2")] + public void ParseFloatsWithChangedCultureInfo_Fails(string culture, string value) + { + // ARRANGE + var posts = new List + { + new Post + { + Id = 1, + Title = "E\\", + LikeCount = 4, + MeanRating = 1.9f, + IsDraft = true, + CategoryId = 1, + TopComment = new Comment { Id = 1, Text = "E1" }, + FeaturedComment = new Comment { Id = 7, Text = "E2" } + } + }.AsQueryable(); + + var optionsAccessor = new SieveOptionsAccessor(); + optionsAccessor.Value.CultureNameOfTypeConversion = culture; + + var processor = new ApplicationSieveProcessor(optionsAccessor, + new SieveCustomSortMethods(), + new SieveCustomFilterMethods()); + + var model = new SieveModel() { Filters = $"MeanRating=={value}" }; + + // ACT, ASSERT + Assert.Throws(() => processor.Apply(model, posts)); + } + } }