Where() and Select() methods from strings expression parsing improved (#1972)

* Add ExpressionParser and tests.

* Should_SupportNullableShortParsing test added

* Support conversion for binary operations

* Should_SupportNullablePropertiesWithArray test added

* Should_SupportDateTimeWithArray added

* code fixed

* Compilation moved to Select() only

* DataGrid columns filtering temporary switched to string expressions

* typeLocator added

* Convert arguments if needed.

* Should_SupportNestedLambdasWithEnums added

* locator improved for nested types

* ToFilterString() will not cast enums

* expression fixed

* Revert "demo reworked without strings"

This reverts commit 5e1fa61c55.

* ToFilterString() improved

* Support projections.

* ExpressionParser.ParseProjection added

* code improved

* Select by string.

* Support conditional access expressions.

* null condition added

* should add item instance name only to non indexer properties

* Array and dictionary access.

* Add editorconfig.

* Dynamic property еxpression updated to cast

* ToFilterString() removed

* Where() and Select() exception handling improved

---------

Co-authored-by: Atanas Korchev <akorchev@gmail.com>
This commit is contained in:
Vladimir Enchev
2025-02-17 10:42:58 +02:00
committed by GitHub
parent 5e1fa61c55
commit ee5674bb6d
8 changed files with 1029 additions and 714 deletions

244
.editorconfig Normal file
View File

@@ -0,0 +1,244 @@
# Remove the line below if you want to inherit .editorconfig settings from higher directories
root = true
# C# files
[*.cs]
#### Core EditorConfig Options ####
# Indentation and spacing
indent_size = 4
indent_style = space
tab_width = 4
# New line preferences
end_of_line = crlf
insert_final_newline = false
#### .NET Code Actions ####
# Type members
dotnet_hide_advanced_members = false
dotnet_member_insertion_location = with_other_members_of_the_same_kind
dotnet_property_generation_behavior = prefer_throwing_properties
# Symbol search
dotnet_search_reference_assemblies = true
#### .NET Coding Conventions ####
# Organize usings
dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = false
file_header_template = unset
# this. and Me. preferences
dotnet_style_qualification_for_event = false
dotnet_style_qualification_for_field = false
dotnet_style_qualification_for_method = false
dotnet_style_qualification_for_property = false
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true
dotnet_style_predefined_type_for_member_access = true
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_operators = never_if_unnecessary
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members
# Expression-level preferences
dotnet_prefer_system_hash_code = true
dotnet_style_coalesce_expression = true
dotnet_style_collection_initializer = true
dotnet_style_explicit_tuple_names = true
dotnet_style_namespace_match_folder = true
dotnet_style_null_propagation = true
dotnet_style_object_initializer = true
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_prefer_auto_properties = true
dotnet_style_prefer_collection_expression = when_types_loosely_match
dotnet_style_prefer_compound_assignment = true
dotnet_style_prefer_conditional_expression_over_assignment = true
dotnet_style_prefer_conditional_expression_over_return = true
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
dotnet_style_prefer_inferred_anonymous_type_member_names = true
dotnet_style_prefer_inferred_tuple_names = true
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
dotnet_style_prefer_simplified_boolean_expressions = true
dotnet_style_prefer_simplified_interpolation = true
# Field preferences
dotnet_style_readonly_field = true
# Parameter preferences
dotnet_code_quality_unused_parameters = all
# Suppression preferences
dotnet_remove_unnecessary_suppression_exclusions = none
# New line preferences
dotnet_style_allow_multiple_blank_lines_experimental = true
dotnet_style_allow_statement_immediately_after_block_experimental = true
#### C# Coding Conventions ####
# var preferences
csharp_style_var_elsewhere = false
csharp_style_var_for_built_in_types = false
csharp_style_var_when_type_is_apparent = false
# Expression-bodied members
csharp_style_expression_bodied_accessors = true
csharp_style_expression_bodied_constructors = false
csharp_style_expression_bodied_indexers = true
csharp_style_expression_bodied_lambdas = true
csharp_style_expression_bodied_local_functions = false
csharp_style_expression_bodied_methods = false
csharp_style_expression_bodied_operators = false
csharp_style_expression_bodied_properties = true
# Pattern matching preferences
csharp_style_pattern_matching_over_as_with_null_check = true
csharp_style_pattern_matching_over_is_with_cast_check = true
csharp_style_prefer_extended_property_pattern = true
csharp_style_prefer_not_pattern = true
csharp_style_prefer_pattern_matching = true
csharp_style_prefer_switch_expression = true
# Null-checking preferences
csharp_style_conditional_delegate_call = true
# Modifier preferences
csharp_prefer_static_anonymous_function = true
csharp_prefer_static_local_function = true
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async
csharp_style_prefer_readonly_struct = true
csharp_style_prefer_readonly_struct_member = true
# Code-block preferences
csharp_prefer_braces = true
csharp_prefer_simple_using_statement = true
csharp_prefer_system_threading_lock = true
csharp_style_namespace_declarations = block_scoped
csharp_style_prefer_method_group_conversion = true
csharp_style_prefer_primary_constructors = true
csharp_style_prefer_top_level_statements = true
# Expression-level preferences
csharp_prefer_simple_default_expression = true
csharp_style_deconstructed_variable_declaration = true
csharp_style_implicit_object_creation_when_type_is_apparent = true
csharp_style_inlined_variable_declaration = true
csharp_style_prefer_index_operator = true
csharp_style_prefer_local_over_anonymous_function = true
csharp_style_prefer_null_check_over_type_check = true
csharp_style_prefer_range_operator = true
csharp_style_prefer_tuple_swap = true
csharp_style_prefer_utf8_string_literals = true
csharp_style_throw_expression = true
csharp_style_unused_value_assignment_preference = discard_variable
csharp_style_unused_value_expression_statement_preference = discard_variable
# 'using' directive preferences
csharp_using_directive_placement = outside_namespace
# New line preferences
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true
csharp_style_allow_embedded_statements_on_same_line_experimental = true
#### C# Formatting Rules ####
# New line preferences
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_labels = no_change
csharp_indent_switch_labels = true
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Wrapping preferences
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
#### Naming styles ####
# Naming rules
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Symbol specifications
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Naming styles
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case

View File

@@ -0,0 +1,288 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using Xunit;
namespace Radzen.Blazor.Tests
{
public enum CarType
{
Sedan,
Coupe
}
public class ExpressionParserTests
{
class Person
{
public short? Age { get; set; }
public string Name { get; set; }
public bool? Famous { get; set; }
public DateTime BirthDate { get; set; }
}
public class Car
{
public CarType Type { get; set; }
}
public enum Status
{
Office,
Remote,
}
class OrderDetail
{
public Product Product { get; set; }
public int Quantity { get; set; }
public Order Order { get; set; }
public List<WorkStatus> WorkStatuses { get; set; }
public List<Status> Statuses { get; set; }
}
class Order
{
public DateTime OrderDate { get; set; }
}
class Product
{
public string ProductName { get; set; }
}
[Fact]
public void Should_ParseBindaryExpression()
{
var expression = ExpressionParser.ParsePredicate<Person>("p => p.Name == \"foo\"");
var func = expression.Compile();
Assert.True(func(new Person() { Name = "foo" }));
}
[Fact]
public void Should_ParseConditionalExpression()
{
var expression = ExpressionParser.ParsePredicate<OrderDetail>("it => (it.Product.ProductName == null ? \"\" : it.Product.ProductName).Contains(\"Queso\") && it.Quantity == 50");
var func = expression.Compile();
Assert.True(func(new OrderDetail() { Product = new Product { ProductName = "Queso" }, Quantity = 50 }));
}
[Fact]
public void Should_ParseNestedLogicalOperations()
{
var expression = ExpressionParser.ParsePredicate<OrderDetail>("it => (it.Product.ProductName == null ? \"\" : it.Product.ProductName).Contains(\"Queso\") && (it.Quantity == 50 || it.Quantity == 12)");
var func = expression.Compile();
Assert.True(func(new OrderDetail() { Product = new Product { ProductName = "Queso" }, Quantity = 12 }));
}
[Fact]
public void Should_SupportDateTimeParsing()
{
var expression = ExpressionParser.ParsePredicate<OrderDetail>("it => it.Order.OrderDate >= DateTime.Parse(\"2025-02-11\")");
var func = expression.Compile();
Assert.True(func(new OrderDetail { Order = new Order { OrderDate = new DateTime(2025, 2, 11) } }));
}
class ItemWithGenericProperty<T>
{
public T Value { get; set; }
}
[Fact]
public void Should_SupportDateOnlyParsing()
{
var expression = ExpressionParser.ParsePredicate<ItemWithGenericProperty<DateOnly>>("it => it.Value >= DateOnly.Parse(\"2025-02-11\")");
var func = expression.Compile();
Assert.True(func(new ItemWithGenericProperty<DateOnly> { Value = new DateOnly(2025, 2, 11) }));
}
[Fact]
public void Should_SupportTimeOnlyParsing()
{
var expression = ExpressionParser.ParsePredicate<ItemWithGenericProperty<TimeOnly>>("it => it.Value >= TimeOnly.Parse(\"12:00:00\")");
var func = expression.Compile();
Assert.True(func(new ItemWithGenericProperty<TimeOnly> { Value = new TimeOnly(12, 0, 0) }));
}
[Fact]
public void Should_SupportGuidParsing()
{
var expression = ExpressionParser.ParsePredicate<ItemWithGenericProperty<Guid>>("it => it.Value == Guid.Parse(\"f0e7e7d8-4f4d-4b5f-8b3e-3f1d1b4f5f5f\")");
var func = expression.Compile();
Assert.True(func(new ItemWithGenericProperty<Guid> { Value = Guid.Parse("f0e7e7d8-4f4d-4b5f-8b3e-3f1d1b4f5f5f") }));
}
[Fact]
public void Should_SupportEnumWithCasts()
{
var typeLocator = (string type) => AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()).FirstOrDefault(t => t.FullName == type);
var expression = ExpressionParser.ParsePredicate<ItemWithGenericProperty<CarType[]>>("it => it.Value.Any(i => (new []{0}).Contains(i))", typeLocator);
var func = expression.Compile();
Assert.True(func(new ItemWithGenericProperty<CarType[]> { Value = [CarType.Sedan] }));
}
[Fact]
public void Should_SupportEnumCollections()
{
var typeLocator = (string type) => AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()).FirstOrDefault(t => t.FullName == type);
var expression = ExpressionParser.ParsePredicate<Car>($"it => it.Type == (Radzen.Blazor.Tests.CarType)1", typeLocator);
var func = expression.Compile();
Assert.True(func(new Car { Type = CarType.Coupe }));
}
[Fact]
public void Should_SupportCollectionLiterals()
{
var expression = ExpressionParser.ParsePredicate<OrderDetail>("it => (new []{\"Tofu\"}).Contains(it.Product.ProductName)");
var func = expression.Compile();
Assert.True(func(new OrderDetail { Product = new Product { ProductName = "Tofu" } }));
}
class WorkStatus
{
public string Name { get; set; }
}
[Fact]
public void Should_SupportNestedLambdas()
{
var expression = ExpressionParser.ParsePredicate<OrderDetail>("it => it.WorkStatuses.Any(i => (new []{\"Office\"}).Contains(i.Name))");
var func = expression.Compile();
Assert.True(func(new OrderDetail { WorkStatuses = [new() { Name = "Office" }] }));
}
[Fact]
public void Should_SupportNestedLambdasWithEnums()
{
var expression = ExpressionParser.ParsePredicate<OrderDetail>("it => it.Statuses.Any(i => (new []{1}).Contains(i))");
var func = expression.Compile();
Assert.True(func(new OrderDetail { Statuses = new List<Status>() { (Status)1 } }));
}
[Fact]
public void Should_SupportToLower()
{
var expression = ExpressionParser.ParsePredicate<Person>("it => (it.Name == null ? \"\" : it.Name).ToLower().Contains(\"na\".ToLower())");
var func = expression.Compile();
Assert.True(func(new Person { Name = "Nana" }));
}
[Fact]
public void Should_SupportNullableProperties()
{
var expression = ExpressionParser.ParsePredicate<Person>("it => it.Age == 50");
var func = expression.Compile();
Assert.True(func(new Person { Age = 50 }));
}
[Fact]
public void Should_SupportNullablePropertiesWithArray()
{
var expression = ExpressionParser.ParsePredicate<Person>("it => (new []{}).Contains(it.Famous)");
var func = expression.Compile();
Assert.False(func(new Person { Famous = null }));
}
[Fact]
public void Should_SupportDateTimeWithArray()
{
var expression = ExpressionParser.ParsePredicate<Person>("it => (new []{DateTime.Parse(\"5/5/2000 12:00:00 AM\")}).Contains(it.BirthDate)");
var func = expression.Compile();
Assert.True(func(new Person { BirthDate = DateTime.Parse("5/5/2000 12:00:00 AM") }));
}
[Fact]
public void Should_SupportNumericConversion()
{
var expression = ExpressionParser.ParsePredicate<ItemWithGenericProperty<double>>("it => it.Value == 50");
var func = expression.Compile();
Assert.True(func(new ItemWithGenericProperty<double> { Value = 50.0 }));
}
[Fact]
public void Should_CreateProjection()
{
var expression = ExpressionParser.ParseLambda<OrderDetail>("it => new { ProductName = it.Product.ProductName}");
var func = expression.Compile();
var result = func.DynamicInvoke(new OrderDetail { Product = new Product { ProductName = "Queso" } });
Assert.Equal("Queso", result.GetType().GetProperty("ProductName").GetValue(result));
}
[Fact]
public void Should_CreateProjectionFromConditionalAccess()
{
var expression = ExpressionParser.ParseLambda<OrderDetail>("it => new { ProductName = it.Product?.ProductName}");
var func = expression.Compile();
var result = func.DynamicInvoke(new OrderDetail { Product = null });
Assert.Null(result.GetType().GetProperty("ProductName").GetValue(result));
}
[Fact]
public void Should_SelectByString()
{
var list = new List<OrderDetail> { new OrderDetail { Product = new Product { ProductName = "Chai" } } }.AsQueryable();
var result = DynamicExtensions.Select(list, "Product.ProductName as ProductName");
Assert.Equal("Chai", result.ElementType.GetProperty("ProductName").GetValue(result.FirstOrDefault()));
}
[Fact]
public void Should_SupportDictionaryIndexAccess()
{
var expression = ExpressionParser.ParsePredicate<Dictionary<string, object>>("it => (int)it[\"foo\"] == 1");
var func = expression.Compile();
Assert.True(func(new Dictionary<string, object> { ["foo"] = 1 }));
}
[Fact]
public void Should_SupportDictionaryIndexAccessWithNullableCast()
{
var expression = ExpressionParser.ParsePredicate<Dictionary<string, object>>("it => (Int32?)it[\"foo\"] == null");
var func = expression.Compile();
Assert.True(func(new Dictionary<string, object> { ["foo"] = null }));
}
[Fact]
public void Should_SupportArrayIndexAccess()
{
var expression = ExpressionParser.ParsePredicate<ItemWithGenericProperty<int[]>>("it => it.Value[0] == 1");
var func = expression.Compile();
Assert.True(func(new ItemWithGenericProperty<int[]> { Value = [1] }));
}
}
}

View File

@@ -3402,7 +3402,7 @@ namespace Radzen
var typeName = isEnum ? "Enum" : (Nullable.GetUnderlyingType(type) ?? type).Name;
var typeFunc = $@"{typeName}{(!isEnum && Nullable.GetUnderlyingType(type) != null ? "?" : "")}";
return $@"{typeFunc}(it[""{name}""])";
return $@"({typeFunc})it[""{name}""]";
}
}

View File

@@ -1,22 +1,5 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Radzen;
using Radzen.Blazor;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Common;
using System.Globalization;
using System.IO;
using System.Linq;
using Radzen;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using System.Reflection.Metadata;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;
using System.Text;
using System.Text.RegularExpressions;
namespace System.Linq.Dynamic.Core
{
@@ -25,45 +8,8 @@ namespace System.Linq.Dynamic.Core
/// </summary>
public static class DynamicExtensions
{
static string rtPath = Path.GetDirectoryName(typeof(object).Assembly.Location);
static CSharpCompilation Compilation = CSharpCompilation.Create(Guid.NewGuid().ToString())
.WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
.WithOptimizationLevel(OptimizationLevel.Debug))
.AddReferences(MetadataReference.CreateFromFile(Path.Combine(rtPath, "System.Runtime.dll")))
.AddReferences(MetadataReference.CreateFromFile(Path.Combine(rtPath, "System.Collections.dll")))
.AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location))
.AddReferences(MetadataReference.CreateFromFile(typeof(Expression).Assembly.Location))
.AddReferences(MetadataReference.CreateFromFile(typeof(Queryable).Assembly.Location))
.AddReferences(MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location));
private static Assembly Compile(CSharpCompilation compilation, string code, AssemblyLoadContext context)
{
var errors = compilation.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error);
if (errors.Any())
{
var message = string.Join(Environment.NewLine, errors.Select(e => e.GetMessage()));
throw new InvalidOperationException($"Compilation of {code} failed: {message}");
}
using var projectStream = new MemoryStream();
var result = compilation.Emit(projectStream);
if (!result.Success)
{
errors = result.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error);
var message = string.Join(Environment.NewLine, errors.Select(e => e.GetMessage()));
throw new InvalidOperationException(message);
}
projectStream.Seek(0, SeekOrigin.Begin);
return context.LoadFromStream(projectStream);
}
static Func<string, Type> typeLocator = type => AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => a.GetTypes()).FirstOrDefault(t => t.FullName.Replace("+", ".") == type);
/// <summary>
/// Filters using the specified filter descriptors.
@@ -75,42 +21,32 @@ namespace System.Linq.Dynamic.Core
{
try
{
if (parameters != null)
if (parameters != null && !string.IsNullOrEmpty(selector))
{
for (var i = 0; i < parameters.Length; i++)
{
var value = object.Equals(parameters[i], string.Empty) ? @"""""" :
parameters[i] == null ? @"null" :
parameters[i] is string ? @$"""{parameters[i].ToString().Replace("\"", "\\\"")}""" :
parameters[i] is string ? @$"""{parameters[i].ToString().Replace("\"", "\\\"")}""" :
parameters[i] is bool ? $"{parameters[i]}".ToLower() : parameters[i];
selector = selector.Replace($"@{i}", $"{value}");
}
}
var code = $@"
using System;
using System.Linq;
using System.Linq.Expressions;
namespace Dynamic;
public static class Linq
{{
public static Expression<Func<{typeof(T).FullName.Replace("+", ".")}, bool>> where = {(selector == "true" ? "i => true" : selector).Replace("DateTime", "DateTime.Parse").Replace("DateTimeOffset", "DateTimeOffset.Parse").Replace("DateOnly", "DateOnly.Parse").Replace("Guid", "Guid.Parse").Replace(" = "," == ")};
}}";
selector = (selector == "true" ? "" : selector)
.Replace("DateTime(", "DateTime.Parse(")
.Replace("DateTimeOffset(", "DateTimeOffset.Parse(")
.Replace("DateOnly(", "DateOnly.Parse(")
.Replace("Guid(", "Guid.Parse(")
.Replace(" = ", " == ");
var assembly = Compile(Compilation
.AddReferences(MetadataReference.CreateFromFile(typeof(T).Assembly.Location))
.AddSyntaxTrees(CSharpSyntaxTree.ParseText(code, new CSharpParseOptions(LanguageVersion.Latest))),
code, new AssemblyLoadContext("RadzenALC", true));
Expression<Func<T, bool>> whereMethod = (Expression<Func<T, bool>>)assembly
.GetType("Dynamic.Linq").GetFields().FirstOrDefault().GetValue(null);
return source.Where(whereMethod);
return !string.IsNullOrEmpty(selector) ?
source.Where(ExpressionParser.ParsePredicate<T>(selector, typeLocator)) : source;
}
catch
catch (Exception ex)
{
throw new InvalidOperationException($"Invalid Where selector");
throw new InvalidOperationException($"Invalid Where selector: '{selector}'. Exception: {ex.Message}");
}
}
@@ -135,619 +71,28 @@ public static class Linq
{
try
{
var parameter = Expression.Parameter(typeof(T), "it");
var className = $"Class{Guid.NewGuid()}".Replace("-", "");
var properties = selector.Replace("new (", "").Replace(")", "").Trim().Split(",", StringSplitOptions.RemoveEmptyEntries);
var declaredProperties = string.Join(Environment.NewLine, properties
.Select(s =>
{
var original = s.Split(" as ").FirstOrDefault().Trim();
var property = QueryableExtension.GetNestedPropertyExpression(parameter, original);
var name = s.Contains(" as ") ? s.Split(" as ").LastOrDefault().Trim() : s.Trim();
return $@"public {property.Type.DisplayName(fullName: false) + (original.Contains(".") ? "?" : "")} {name} {{ get; set; }}";
}));
var properties = selector
.Replace("new (", "").Replace(")", "").Replace("new {", "").Replace("}", "").Trim()
.Split(",", StringSplitOptions.RemoveEmptyEntries);
selector = string.Join(", ", properties
.Select(s => (s.Contains(" as ") ? s.Split(" as ").LastOrDefault().Trim() : s.Trim()) + " = " + $"it.{s.Split(" as ").FirstOrDefault().Replace(".", "?.").Trim()}"));
.Select(s => (s.Contains(" as ") ? s.Split(" as ").LastOrDefault().Trim() : s.Trim()) +
" = " + $"it.{s.Split(" as ").FirstOrDefault().Replace(".", "?.").Trim()}"));
var code = $@"
using System;
using System.Linq;
using System.Linq.Expressions;
namespace Dynamic;
public class {className}
{{
{declaredProperties}
}}
public static class Linq
{{
public static IQueryable Select(IQueryable source)
{{
var list = System.Linq.Enumerable.ToList(System.Linq.Queryable.Cast<{typeof(T).FullName.Replace("+", ".")}>(source));
return System.Linq.Queryable.AsQueryable(list.Select(it => new {className}() {{ {selector} }}));
}}
}}";
var assembly = Compile(Compilation
.AddReferences(MetadataReference.CreateFromFile(typeof(T).Assembly.Location))
.AddSyntaxTrees(CSharpSyntaxTree.ParseText(code, new CSharpParseOptions(LanguageVersion.Latest))),
code, new AssemblyLoadContext("RadzenALC", true));
return (IQueryable)assembly.GetType("Dynamic.Linq").GetMethods().FirstOrDefault().Invoke(null, new object[] { source });
}
catch
{
throw new InvalidOperationException($"Invalid selector");
}
}
}
static class SharedTypeExtensions
{
private static readonly Dictionary<Type, string> BuiltInTypeNames = new()
{
{ typeof(bool), "bool" },
{ typeof(byte), "byte" },
{ typeof(char), "char" },
{ typeof(decimal), "decimal" },
{ typeof(double), "double" },
{ typeof(float), "float" },
{ typeof(int), "int" },
{ typeof(long), "long" },
{ typeof(object), "object" },
{ typeof(sbyte), "sbyte" },
{ typeof(short), "short" },
{ typeof(string), "string" },
{ typeof(uint), "uint" },
{ typeof(ulong), "ulong" },
{ typeof(ushort), "ushort" },
{ typeof(void), "void" }
};
public static Type UnwrapNullableType(this Type type)
=> Nullable.GetUnderlyingType(type) ?? type;
public static bool IsNullableValueType(this Type type)
=> type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
public static bool IsNullableType(this Type type)
=> !type.IsValueType || type.IsNullableValueType();
public static bool IsValidEntityType(this Type type)
=> type.IsClass
&& !type.IsArray;
public static bool IsPropertyBagType(this Type type)
{
if (type.IsGenericTypeDefinition)
{
return false;
}
var types = GetGenericTypeImplementations(type, typeof(IDictionary<,>));
return types.Any(
t => t.GetGenericArguments()[0] == typeof(string)
&& t.GetGenericArguments()[1] == typeof(object));
}
public static Type MakeNullable(this Type type, bool nullable = true)
=> type.IsNullableType() == nullable
? type
: nullable
? typeof(Nullable<>).MakeGenericType(type)
: type.UnwrapNullableType();
public static bool IsNumeric(this Type type)
{
type = type.UnwrapNullableType();
return type.IsInteger()
|| type == typeof(decimal)
|| type == typeof(float)
|| type == typeof(double);
}
public static bool IsInteger(this Type type)
{
type = type.UnwrapNullableType();
return type == typeof(int)
|| type == typeof(long)
|| type == typeof(short)
|| type == typeof(byte)
|| type == typeof(uint)
|| type == typeof(ulong)
|| type == typeof(ushort)
|| type == typeof(sbyte)
|| type == typeof(char);
}
public static bool IsSignedInteger(this Type type)
=> type == typeof(int)
|| type == typeof(long)
|| type == typeof(short)
|| type == typeof(sbyte);
public static bool IsAnonymousType(this Type type)
=> type.Name.StartsWith("<>", StringComparison.Ordinal)
&& type.GetCustomAttributes(typeof(CompilerGeneratedAttribute), inherit: false).Length > 0
&& type.Name.Contains("AnonymousType");
public static PropertyInfo GetAnyProperty(this Type type, string name)
{
var props = type.GetRuntimeProperties().Where(p => p.Name == name).ToList();
if (props.Count > 1)
{
throw new AmbiguousMatchException();
}
return props.SingleOrDefault();
}
public static bool IsInstantiable(this Type type)
=> !type.IsAbstract
&& !type.IsInterface
&& (!type.IsGenericType || !type.IsGenericTypeDefinition);
public static Type UnwrapEnumType(this Type type)
{
var isNullable = type.IsNullableType();
var underlyingNonNullableType = isNullable ? type.UnwrapNullableType() : type;
if (!underlyingNonNullableType.IsEnum)
{
return type;
}
var underlyingEnumType = Enum.GetUnderlyingType(underlyingNonNullableType);
return isNullable ? MakeNullable(underlyingEnumType) : underlyingEnumType;
}
public static Type GetSequenceType(this Type type)
{
var sequenceType = TryGetSequenceType(type);
if (sequenceType == null)
{
throw new ArgumentException($"The type {type.Name} does not represent a sequence");
}
return sequenceType;
}
public static Type TryGetSequenceType(this Type type)
=> type.TryGetElementType(typeof(IEnumerable<>))
?? type.TryGetElementType(typeof(IAsyncEnumerable<>));
public static Type TryGetElementType(this Type type, Type interfaceOrBaseType)
{
if (type.IsGenericTypeDefinition)
{
return null;
}
var types = GetGenericTypeImplementations(type, interfaceOrBaseType);
Type singleImplementation = null;
foreach (var implementation in types)
{
if (singleImplementation == null)
if (string.IsNullOrEmpty(selector))
{
singleImplementation = implementation;
}
else
{
singleImplementation = null;
break;
}
}
return singleImplementation?.GenericTypeArguments.FirstOrDefault();
}
public static bool IsCompatibleWith(this Type propertyType, Type fieldType)
{
if (propertyType.IsAssignableFrom(fieldType)
|| fieldType.IsAssignableFrom(propertyType))
{
return true;
}
var propertyElementType = propertyType.TryGetSequenceType();
var fieldElementType = fieldType.TryGetSequenceType();
return propertyElementType != null
&& fieldElementType != null
&& IsCompatibleWith(propertyElementType, fieldElementType);
}
public static IEnumerable<Type> GetGenericTypeImplementations(this Type type, Type interfaceOrBaseType)
{
var typeInfo = type.GetTypeInfo();
if (!typeInfo.IsGenericTypeDefinition)
{
var baseTypes = interfaceOrBaseType.GetTypeInfo().IsInterface
? typeInfo.ImplementedInterfaces
: type.GetBaseTypes();
foreach (var baseType in baseTypes)
{
if (baseType.IsGenericType
&& baseType.GetGenericTypeDefinition() == interfaceOrBaseType)
{
yield return baseType;
}
return source;
}
if (type.IsGenericType
&& type.GetGenericTypeDefinition() == interfaceOrBaseType)
{
yield return type;
}
var lambda = ExpressionParser.ParseLambda<T>($"it => new {{ {selector} }}");
return source.Provider.CreateQuery(Expression.Call(typeof(Queryable), nameof(Queryable.Select),
[source.ElementType, lambda.Body.Type], source.Expression, Expression.Quote(lambda)));
}
catch (Exception ex)
{
throw new InvalidOperationException($"Invalid Select selector: '{selector}'. Exception: {ex.Message}");
}
}
public static IEnumerable<Type> GetBaseTypes(this Type type)
{
var currentType = type.BaseType;
while (currentType != null)
{
yield return currentType;
currentType = currentType.BaseType;
}
}
public static List<Type> GetBaseTypesAndInterfacesInclusive(this Type type)
{
var baseTypes = new List<Type>();
var typesToProcess = new Queue<Type>();
typesToProcess.Enqueue(type);
while (typesToProcess.Count > 0)
{
type = typesToProcess.Dequeue();
baseTypes.Add(type);
if (type.IsNullableValueType())
{
typesToProcess.Enqueue(Nullable.GetUnderlyingType(type)!);
}
if (type.IsConstructedGenericType)
{
typesToProcess.Enqueue(type.GetGenericTypeDefinition());
}
if (!type.IsGenericTypeDefinition
&& !type.IsInterface)
{
if (type.BaseType != null)
{
typesToProcess.Enqueue(type.BaseType);
}
foreach (var @interface in GetDeclaredInterfaces(type))
{
typesToProcess.Enqueue(@interface);
}
}
}
return baseTypes;
}
public static IEnumerable<Type> GetTypesInHierarchy(this Type type)
{
var currentType = type;
while (currentType != null)
{
yield return currentType;
currentType = currentType.BaseType;
}
}
public static IEnumerable<Type> GetDeclaredInterfaces(this Type type)
{
var interfaces = type.GetInterfaces();
if (type.BaseType == typeof(object)
|| type.BaseType == null)
{
return interfaces;
}
return interfaces.Except(type.BaseType.GetInterfaces());
}
public static ConstructorInfo GetDeclaredConstructor(this Type type, Type[] types)
{
types ??= Array.Empty<Type>();
return type.GetTypeInfo().DeclaredConstructors
.SingleOrDefault(
c => !c.IsStatic
&& c.GetParameters().Select(p => p.ParameterType).SequenceEqual(types))!;
}
public static IEnumerable<PropertyInfo> GetPropertiesInHierarchy(this Type type, string name)
{
var currentType = type;
do
{
var typeInfo = currentType.GetTypeInfo();
foreach (var propertyInfo in typeInfo.DeclaredProperties)
{
if (propertyInfo.Name.Equals(name, StringComparison.Ordinal)
&& !(propertyInfo.GetMethod ?? propertyInfo.SetMethod)!.IsStatic)
{
yield return propertyInfo;
}
}
currentType = typeInfo.BaseType;
}
while (currentType != null);
}
// Looking up the members through the whole hierarchy allows to find inherited private members.
public static IEnumerable<MemberInfo> GetMembersInHierarchy(this Type type)
{
var currentType = type;
do
{
// Do the whole hierarchy for properties first since looking for fields is slower.
foreach (var propertyInfo in currentType.GetRuntimeProperties().Where(pi => !(pi.GetMethod ?? pi.SetMethod)!.IsStatic))
{
yield return propertyInfo;
}
foreach (var fieldInfo in currentType.GetRuntimeFields().Where(f => !f.IsStatic))
{
yield return fieldInfo;
}
currentType = currentType.BaseType;
}
while (currentType != null);
}
public static IEnumerable<MemberInfo> GetMembersInHierarchy(this Type type, string name)
=> type.GetMembersInHierarchy().Where(m => m.Name == name);
private static readonly Dictionary<Type, object> CommonTypeDictionary = new()
{
#pragma warning disable IDE0034 // Simplify 'default' expression - default causes default(object)
{ typeof(int), default(int) },
{ typeof(Guid), default(Guid) },
{ typeof(DateOnly), default(DateOnly) },
{ typeof(DateTime), default(DateTime) },
{ typeof(DateTimeOffset), default(DateTimeOffset) },
{ typeof(TimeOnly), default(TimeOnly) },
{ typeof(long), default(long) },
{ typeof(bool), default(bool) },
{ typeof(double), default(double) },
{ typeof(short), default(short) },
{ typeof(float), default(float) },
{ typeof(byte), default(byte) },
{ typeof(char), default(char) },
{ typeof(uint), default(uint) },
{ typeof(ushort), default(ushort) },
{ typeof(ulong), default(ulong) },
{ typeof(sbyte), default(sbyte) }
#pragma warning restore IDE0034 // Simplify 'default' expression
};
public static object GetDefaultValue(this Type type)
{
if (!type.IsValueType)
{
return null;
}
// A bit of perf code to avoid calling Activator.CreateInstance for common types and
// to avoid boxing on every call. This is about 50% faster than just calling CreateInstance
// for all value types.
return CommonTypeDictionary.TryGetValue(type, out var value)
? value
: Activator.CreateInstance(type);
}
public static IEnumerable<Reflection.TypeInfo> GetConstructibleTypes(this Assembly assembly)
=> assembly.GetLoadableDefinedTypes().Where(
t => !t.IsAbstract
&& !t.IsGenericTypeDefinition);
public static IEnumerable<Reflection.TypeInfo> GetLoadableDefinedTypes(this Assembly assembly)
{
try
{
return assembly.DefinedTypes;
}
catch (ReflectionTypeLoadException ex)
{
return ex.Types.Where(t => t != null).Select(IntrospectionExtensions.GetTypeInfo!);
}
}
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public static string DisplayName(this Type type, bool fullName = true, bool compilable = false)
{
var stringBuilder = new StringBuilder();
ProcessType(stringBuilder, type, fullName, compilable);
return stringBuilder.ToString();
}
private static void ProcessType(StringBuilder builder, Type type, bool fullName, bool compilable)
{
if (type.IsGenericType)
{
var genericArguments = type.GetGenericArguments();
ProcessGenericType(builder, type, genericArguments, genericArguments.Length, fullName, compilable);
}
else if (type.IsArray)
{
ProcessArrayType(builder, type, fullName, compilable);
}
else if (BuiltInTypeNames.TryGetValue(type, out var builtInName))
{
builder.Append(builtInName);
}
else if (!type.IsGenericParameter)
{
if (compilable)
{
if (type.IsNested)
{
ProcessType(builder, type.DeclaringType!, fullName, compilable);
builder.Append('.');
}
else if (fullName)
{
builder.Append(type.Namespace).Append('.');
}
builder.Append(type.Name);
}
else
{
builder.Append(fullName ? type.FullName : type.Name);
}
}
}
private static void ProcessArrayType(StringBuilder builder, Type type, bool fullName, bool compilable)
{
var innerType = type;
while (innerType.IsArray)
{
innerType = innerType.GetElementType()!;
}
ProcessType(builder, innerType, fullName, compilable);
while (type.IsArray)
{
builder.Append('[');
builder.Append(',', type.GetArrayRank() - 1);
builder.Append(']');
type = type.GetElementType()!;
}
}
private static void ProcessGenericType(
StringBuilder builder,
Type type,
Type[] genericArguments,
int length,
bool fullName,
bool compilable)
{
if (type.IsConstructedGenericType
&& type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
ProcessType(builder, type.UnwrapNullableType(), fullName, compilable);
builder.Append('?');
return;
}
var offset = type.IsNested ? type.DeclaringType!.GetGenericArguments().Length : 0;
if (compilable)
{
if (type.IsNested)
{
ProcessType(builder, type.DeclaringType!, fullName, compilable);
builder.Append('.');
}
else if (fullName)
{
builder.Append(type.Namespace);
builder.Append('.');
}
}
else
{
if (fullName)
{
if (type.IsNested)
{
ProcessGenericType(builder, type.DeclaringType!, genericArguments, offset, fullName, compilable);
builder.Append('+');
}
else
{
builder.Append(type.Namespace);
builder.Append('.');
}
}
}
var genericPartIndex = type.Name.IndexOf('`');
if (genericPartIndex <= 0)
{
builder.Append(type.Name);
return;
}
builder.Append(type.Name, 0, genericPartIndex);
builder.Append('<');
for (var i = offset; i < length; i++)
{
ProcessType(builder, genericArguments[i], fullName, compilable);
if (i + 1 == length)
{
continue;
}
builder.Append(',');
if (!genericArguments[i + 1].IsGenericParameter)
{
builder.Append(' ');
}
}
builder.Append('>');
}
public static IEnumerable<string> GetNamespaces(this Type type)
{
if (BuiltInTypeNames.ContainsKey(type))
{
yield break;
}
yield return type.Namespace!;
if (type.IsGenericType)
{
foreach (var typeArgument in type.GenericTypeArguments)
{
foreach (var ns in typeArgument.GetNamespaces())
{
yield return ns;
}
}
}
}
public static ConstantExpression GetDefaultValueConstant(this Type type)
=> (ConstantExpression)GenerateDefaultValueConstantMethod
.MakeGenericMethod(type).Invoke(null, Array.Empty<object>())!;
private static readonly MethodInfo GenerateDefaultValueConstantMethod =
typeof(SharedTypeExtensions).GetTypeInfo().GetDeclaredMethod(nameof(GenerateDefaultValueConstant))!;
private static ConstantExpression GenerateDefaultValueConstant<TDefault>()
=> Expression.Constant(default(TDefault), typeof(TDefault));
}
}

View File

@@ -0,0 +1,417 @@
using System;
using System.Linq.Expressions;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Collections.Concurrent;
using System.Reflection.Emit;
namespace Radzen;
static class DynamicTypeFactory
{
public static Type CreateType(string typeName, string[] propertyNames, Type[] propertyTypes)
{
if (propertyNames.Length != propertyTypes.Length)
{
throw new ArgumentException("Property names and types count mismatch.");
}
var assemblyName = new AssemblyName("DynamicTypesAssembly");
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("DynamicTypesModule");
var typeBuilder = moduleBuilder.DefineType(typeName, TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.Sealed);
for (int i = 0; i < propertyNames.Length; i++)
{
var fieldBuilder = typeBuilder.DefineField("_" + propertyNames[i], propertyTypes[i], FieldAttributes.Private);
var propertyBuilder = typeBuilder.DefineProperty(propertyNames[i], PropertyAttributes.None, propertyTypes[i], null);
var getterMethod = typeBuilder.DefineMethod(
"get_" + propertyNames[i],
MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
propertyTypes[i],
Type.EmptyTypes);
var getterIl = getterMethod.GetILGenerator();
getterIl.Emit(OpCodes.Ldarg_0);
getterIl.Emit(OpCodes.Ldfld, fieldBuilder);
getterIl.Emit(OpCodes.Ret);
propertyBuilder.SetGetMethod(getterMethod);
var setterMethod = typeBuilder.DefineMethod(
"set_" + propertyNames[i],
MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
null,
new[] { propertyTypes[i] });
var setterIl = setterMethod.GetILGenerator();
setterIl.Emit(OpCodes.Ldarg_0);
setterIl.Emit(OpCodes.Ldarg_1);
setterIl.Emit(OpCodes.Stfld, fieldBuilder);
setterIl.Emit(OpCodes.Ret);
propertyBuilder.SetSetMethod(setterMethod);
}
var dynamicType = typeBuilder.CreateType();
return dynamicType;
}
}
class ExpressionSyntaxVisitor<T> : CSharpSyntaxVisitor<Expression>
{
private readonly ParameterExpression parameter;
private readonly Func<string, Type> typeLocator;
public ExpressionSyntaxVisitor(ParameterExpression parameter, Func<string, Type> typeLocator)
{
this.parameter = parameter;
this.typeLocator = typeLocator;
}
public override Expression VisitBinaryExpression(BinaryExpressionSyntax node)
{
var left = Visit(node.Left);
var right = ConvertIfNeeded(Visit(node.Right), left.Type);
return Expression.MakeBinary(ParseBinaryOperator(node.OperatorToken), left, right);
}
public override Expression VisitMemberAccessExpression(MemberAccessExpressionSyntax node)
{
var expression = Visit(node.Expression) ?? parameter;
return Expression.PropertyOrField(expression, node.Name.Identifier.Text);
}
public override Expression VisitLiteralExpression(LiteralExpressionSyntax node)
{
return Expression.Constant(ParseLiteral(node));
}
public override Expression VisitIdentifierName(IdentifierNameSyntax node)
{
if (node.Identifier.Text == parameter.Name)
{
return parameter;
}
return node.Identifier.Text switch
{
nameof(DateTime) => Expression.Constant(typeof(DateTime)),
nameof(DateOnly) => Expression.Constant(typeof(DateOnly)),
nameof(TimeOnly) => Expression.Constant(typeof(TimeOnly)),
nameof(Guid) => Expression.Constant(typeof(Guid)),
_ => throw new NotSupportedException("Unsupported identifier: " + node.Identifier.Text),
};
}
public override Expression VisitConditionalExpression(ConditionalExpressionSyntax node)
{
var condition = Visit(node.Condition);
var whenTrue = Visit(node.WhenTrue);
var whenFalse = Visit(node.WhenFalse);
return Expression.Condition(condition, whenTrue, whenFalse);
}
public override Expression VisitParenthesizedExpression(ParenthesizedExpressionSyntax node)
{
return Visit(node.Expression);
}
public override Expression VisitCastExpression(CastExpressionSyntax node)
{
var typeName = node.Type.ToString();
var nullable = typeName.EndsWith("?");
if (nullable)
{
typeName = typeName[..^1];
}
var targetType = typeName switch
{
nameof(Int32) => typeof(int),
nameof(Int64) => typeof(long),
nameof(Double) => typeof(double),
nameof(Single) => typeof(float),
nameof(Decimal) => typeof(decimal),
nameof(String) => typeof(string),
nameof(Boolean) => typeof(bool),
nameof(DateTime) => typeof(DateTime),
nameof(DateOnly) => typeof(DateOnly),
nameof(TimeOnly) => typeof(TimeOnly),
nameof(Guid) => typeof(Guid),
nameof(Char) => typeof(char),
"int" => typeof(int),
"long" => typeof(long),
"double" => typeof(double),
"float" => typeof(float),
"decimal" => typeof(decimal),
"string" => typeof(string),
"char" => typeof(char),
_ => typeLocator?.Invoke(typeName)
};
if (nullable)
{
targetType = typeof(Nullable<>).MakeGenericType(targetType);
}
if (targetType == null)
{
throw new NotSupportedException("Unsupported cast type: " + node.Type);
}
var operand = Visit(node.Expression);
return Expression.Convert(operand, targetType);
}
public override Expression VisitImplicitArrayCreationExpression(ImplicitArrayCreationExpressionSyntax node)
{
var expressions = node.Initializer.Expressions.Select(Visit).ToArray();
var elementType = expressions.Length > 0 ? expressions[0].Type : typeof(object);
return Expression.NewArrayInit(elementType, expressions);
}
private Expression CallStaticMethod(Type type, string methodName, Expression[] arguments, Type[] argumentTypes)
{
var methodInfo = type.GetMethod(methodName, BindingFlags.Public | BindingFlags.Static, argumentTypes);
if (methodInfo != null)
{
return Expression.Call(methodInfo, arguments);
}
throw new NotSupportedException("Method not found: " + methodName);
}
public override Expression DefaultVisit(SyntaxNode node)
{
throw new NotSupportedException("Unsupported syntax: " + node.GetType().Name);
}
public override Expression VisitSimpleLambdaExpression(SimpleLambdaExpressionSyntax node)
{
var body = Visit(node.Body);
return Expression.Lambda(body, parameter);
}
private Expression VisitArgument(Expression instance, ArgumentSyntax argument)
{
if (argument.Expression is SimpleLambdaExpressionSyntax lambda)
{
var itemType = GetItemType(instance.Type);
var visitor = new ExpressionSyntaxVisitor<T>(Expression.Parameter(itemType, lambda.Parameter.Identifier.Text), typeLocator);
return visitor.Visit(lambda);
}
return Visit(argument.Expression);
}
private static Expression ConvertIfNeeded(Expression expression, Type targetType)
{
if (expression is not LambdaExpression)
{
return expression.Type == targetType ? expression : Expression.Convert(expression, targetType);
}
return expression;
}
public override Expression VisitInvocationExpression(InvocationExpressionSyntax node)
{
if (node.Expression is MemberAccessExpressionSyntax methodCall)
{
var instance = Visit(methodCall.Expression);
var arguments = node.ArgumentList.Arguments.Select(a => VisitArgument(instance, a)).ToArray();
var argumentTypes = arguments.Select(a => a.Type).ToArray();
if (instance is ConstantExpression constant && constant.Value is Type type)
{
return CallStaticMethod(type, methodCall.Name.Identifier.Text, arguments, argumentTypes);
}
var instanceType = instance.Type;
var methodInfo = instanceType.GetMethod(methodCall.Name.Identifier.Text, BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static, argumentTypes);
if (methodInfo == null)
{
methodInfo = typeof(Enumerable)
.GetMethods(BindingFlags.Public | BindingFlags.Static)
.FirstOrDefault(m => m.Name == methodCall.Name.Identifier.Text && m.GetParameters().Length == arguments.Length + 1);
if (methodInfo != null)
{
var itemType = GetItemType(instanceType);
var genericMethod = methodInfo.MakeGenericMethod(itemType);
return Expression.Call(genericMethod, new[] { instance }.Concat(arguments.Select(a => ConvertIfNeeded(a, itemType))));
}
}
if (methodInfo == null)
{
throw new NotSupportedException("Unsupported method call: " + methodCall.Name.Identifier.Text);
}
return Expression.Call(instance, methodInfo, arguments);
}
throw new NotSupportedException("Unsupported invocation expression: " + node.ToString());
}
private static Type GetItemType(Type enumerableOrArray)
{
return enumerableOrArray.IsArray ? enumerableOrArray.GetElementType() : enumerableOrArray.GetGenericArguments()[0];
}
private static object ParseLiteral(LiteralExpressionSyntax literal)
{
return literal.Kind() switch
{
SyntaxKind.StringLiteralExpression => literal.Token.ValueText,
SyntaxKind.NumericLiteralExpression => literal.Token.Value,
SyntaxKind.TrueLiteralExpression => true,
SyntaxKind.FalseLiteralExpression => false,
SyntaxKind.NullLiteralExpression => null,
_ => throw new NotSupportedException("Unsupported literal: " + literal),
};
}
private static ExpressionType ParseBinaryOperator(SyntaxToken token)
{
return token.Kind() switch
{
SyntaxKind.EqualsEqualsToken => ExpressionType.Equal,
SyntaxKind.LessThanToken => ExpressionType.LessThan,
SyntaxKind.GreaterThanToken => ExpressionType.GreaterThan,
SyntaxKind.LessThanEqualsToken => ExpressionType.LessThanOrEqual,
SyntaxKind.GreaterThanEqualsToken => ExpressionType.GreaterThanOrEqual,
SyntaxKind.ExclamationEqualsToken => ExpressionType.NotEqual,
SyntaxKind.AmpersandAmpersandToken => ExpressionType.AndAlso,
SyntaxKind.BarBarToken => ExpressionType.OrElse,
_ => throw new NotSupportedException("Unsupported operator: " + token.Text),
};
}
public override Expression VisitAnonymousObjectCreationExpression(AnonymousObjectCreationExpressionSyntax node)
{
var properties = node.Initializers.Select(init =>
{
var name = init.NameEquals?.Name.Identifier.Text ?? ((IdentifierNameSyntax)init.Expression).ToString();
var value = Visit(init.Expression);
return new { Name = name, Value = value };
}).ToList();
var propertyNames = properties.Select(p => p.Name).ToArray();
var propertyTypes = properties.Select(p => p.Value.Type).ToArray();
var dynamicType = DynamicTypeFactory.CreateType(typeof(T).Name, propertyNames, propertyTypes);
var bindings = properties.Select(p => Expression.Bind(dynamicType.GetProperty(p.Name), p.Value));
return Expression.MemberInit(Expression.New(dynamicType), bindings);
}
public override Expression VisitConditionalAccessExpression(ConditionalAccessExpressionSyntax node)
{
var expression = Visit(node.Expression);
var whenNotNull = Visit(node.WhenNotNull);
if (expression.Type.IsValueType && Nullable.GetUnderlyingType(expression.Type) == null)
{
throw new NotSupportedException("Conditional access is not supported on non-nullable value types: " + expression.Type);
}
if (!expression.Type.IsValueType || Nullable.GetUnderlyingType(expression.Type) != null)
{
return Expression.Condition(Expression.NotEqual(expression, Expression.Constant(null, expression.Type)),
whenNotNull, Expression.Default(whenNotNull.Type)
);
}
return whenNotNull;
}
public override Expression VisitMemberBindingExpression(MemberBindingExpressionSyntax node)
{
if (node.Parent is ConditionalAccessExpressionSyntax conditional)
{
var expression = Visit(conditional.Expression);
var memberName = node.Name.Identifier.Text;
return Expression.PropertyOrField(expression, memberName);
}
throw new NotSupportedException("Unsupported member binding: " + node.ToString());
}
public override Expression VisitElementAccessExpression(ElementAccessExpressionSyntax node)
{
var expression = Visit(node.Expression);
var arguments = node.ArgumentList.Arguments.Select(arg => Visit(arg.Expression)).ToArray();
if (expression.Type.IsArray)
{
return Expression.ArrayIndex(expression, arguments);
}
var indexer = expression.Type.GetProperties()
.FirstOrDefault(p => p.GetIndexParameters().Length == arguments.Length);
if (indexer != null)
{
return Expression.MakeIndex(expression, indexer, arguments);
}
throw new NotSupportedException("Unsupported element access: " + node.ToString());
}
}
public static class ExpressionParser
{
public static Expression<Func<T, bool>> ParsePredicate<T>(string expression, Func<string, Type> typeLocator = null)
{
return ParseExpression<T, bool>(expression, typeLocator);
}
public static Expression<Func<T, TResult>> ParseExpression<T, TResult>(string expression, Func<string, Type> typeLocator = null)
{
var syntaxTree = CSharpSyntaxTree.ParseText(expression);
var root = syntaxTree.GetRoot();
var lambdaExpression = root.DescendantNodes().OfType<SimpleLambdaExpressionSyntax>().FirstOrDefault();
if (lambdaExpression == null)
{
throw new ArgumentException("Invalid lambda expression.");
}
var parameter = Expression.Parameter(typeof(T), lambdaExpression.Parameter.Identifier.Text);
var visitor = new ExpressionSyntaxVisitor<T>(parameter, typeLocator);
var body = visitor.Visit(lambdaExpression.Body);
return Expression.Lambda<Func<T, TResult>>(body, parameter);
}
public static LambdaExpression ParseLambda<T>(string expression, Func<string, Type> typeLocator = null)
{
var syntaxTree = CSharpSyntaxTree.ParseText(expression);
var root = syntaxTree.GetRoot();
var lambdaExpression = root.DescendantNodes().OfType<SimpleLambdaExpressionSyntax>().FirstOrDefault();
if (lambdaExpression == null)
{
throw new ArgumentException("Invalid lambda expression.");
}
var parameter = Expression.Parameter(typeof(T), lambdaExpression.Parameter.Identifier.Text);
var visitor = new ExpressionSyntaxVisitor<T>(parameter, typeLocator);
var body = visitor.Visit(lambdaExpression.Body);
return Expression.Lambda(body, parameter);
}
}

View File

@@ -587,13 +587,32 @@ namespace Radzen
var booleanOperator = column.LogicalFilterOperator == LogicalFilterOperator.And ? "&&" : "||";
var property = "it." + PropertyAccess.GetProperty(column.GetFilterProperty());
var filterProperty = column.GetFilterProperty();
var itemInstanceName = !filterProperty.Contains("[") ? "it." : "";
var property = itemInstanceName + PropertyAccess.GetProperty(column.GetFilterProperty());
if (sv == null)
{
if (columnFilterOperator == FilterOperator.Contains || columnFilterOperator == FilterOperator.DoesNotContain)
{
whereList.Add($@"{(columnFilterOperator == FilterOperator.DoesNotContain ? "! " : "")}({enumerableValueAsString}).Contains({property})");
if (column.GetFilterValue() is string && column.Property != column.FilterProperty)
{
whereList.Add($@"{(columnFilterOperator == FilterOperator.DoesNotContain ? "! " : "")}{itemInstanceName + column.Property}.Any(i => {"i." + column.FilterProperty}.Contains(""" + column.GetFilterValue() + "\"))");
}
else if (IsEnumerable(column.FilterPropertyType) && column.FilterPropertyType != typeof(string) &&
IsEnumerable(column.PropertyType) && column.PropertyType != typeof(string))
{
whereList.Add($@"{(columnFilterOperator == FilterOperator.DoesNotContain ? "! " : "")}({enumerableValueAsString}).Contains({property})");
}
else if (IsEnumerable(column.FilterPropertyType) && column.FilterPropertyType != typeof(string) &&
column.Property != column.FilterProperty && !string.IsNullOrEmpty(column.FilterProperty))
{
whereList.Add($@"({property}).{(columnFilterOperator == FilterOperator.NotIn ? "Except" : "Intersect")}({enumerableValueAsString}).Any()");
}
else
{
whereList.Add($@"{(columnFilterOperator == FilterOperator.DoesNotContain ? "! " : "")}({enumerableValueAsString}).Contains({property})");
}
}
else if (columnFilterOperator == FilterOperator.In || columnFilterOperator == FilterOperator.NotIn)
{
@@ -605,7 +624,7 @@ namespace Radzen
else if (IsEnumerable(column.FilterPropertyType) && column.FilterPropertyType != typeof(string) &&
column.Property != column.FilterProperty && !string.IsNullOrEmpty(column.FilterProperty))
{
whereList.Add($@"{(columnFilterOperator == FilterOperator.NotIn ? "!" : "")}{column.Property}.Any(i => ({enumerableValueAsString}).Contains(i.{column.FilterProperty}))");
whereList.Add($@"{(columnFilterOperator == FilterOperator.NotIn ? "!" : "")}{itemInstanceName + column.Property}.Any(i => ({enumerableValueAsString}).Contains(i.{column.FilterProperty}))");
}
}
}
@@ -889,7 +908,9 @@ namespace Radzen
/// <returns>System.String.</returns>
private static string GetColumnFilter<T>(RadzenDataGridColumn<T> column, string value, bool second = false)
{
var property = "it." + PropertyAccess.GetProperty(column.GetFilterProperty());
var filterProperty = column.GetFilterProperty();
var itemInstanceName = !filterProperty.Contains("[") ? "it." : "";
var property = itemInstanceName + PropertyAccess.GetProperty(filterProperty);
var propertyType = !string.IsNullOrEmpty(property) ? PropertyAccess.GetPropertyType(typeof(T), property) : null;
string npProperty = (propertyType != null ? Nullable.GetUnderlyingType(propertyType) != null : true) ? $@"({property} ?? null)" : property;
@@ -969,7 +990,7 @@ namespace Radzen
}
else
{
return $"{property} {linqOperator} ({column.FilterPropertyType.FullName.Replace("+",".")}){value}";
return $"{property} {linqOperator} {value}";
}
}
else if (PropertyAccess.IsNumeric(column.FilterPropertyType))

View File

@@ -13,6 +13,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RadzenBlazorDemos.Host", "R
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RadzenBlazorDemos.Server", "RadzenBlazorDemos.Server\RadzenBlazorDemos.Server.csproj", "{EC869401-304A-45BC-93CA-C1CDFAAA7F7B}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{972C3730-C322-4A6F-A106-18D53BF8E08D}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU

View File

@@ -56,10 +56,11 @@ else
DateTime? hireDate = null;
TitleOfCourtesy? currentTOC;
List<string> selectedWorkStatus;
string whereExpression;
public LogicalFilterOperator workStatusOperator { get; set; } = LogicalFilterOperator.Or;
List<string> companyNames = new List<string> { "Vins et alcools Chevalier", "Toms Spezialitäten", "Hanari Carnes", "Richter Supermarkt", "Wellington Importadora", "Centro comercial Moctezuma" };
List<string> companyNames = new List<string> {"Vins et alcools Chevalier", "Toms Spezialitäten", "Hanari Carnes", "Richter Supermarkt", "Wellington Importadora", "Centro comercial Moctezuma" };
List<string> workStatuses = new List<string> { "Office", "Remote", "Temporary", "Permanent" };
public enum TitleOfCourtesy
{
@@ -85,30 +86,27 @@ else
}
}
IEnumerable<Employee> allEmployees;
IEnumerable<Employee> employees;
IEnumerable<Employee> employees;
protected override async Task OnInitializedAsync()
{
allEmployees = await Task.FromResult(Enumerable.Range(0, 60).Select(i =>
employees = await Task.FromResult(Enumerable.Range(0, 60).Select(i =>
new Employee
{
ID = i,
CompanyName = companyNames[i % 6],
HireDate = DateTime.Now.AddMonths(-i),
TitleOfCourtesy = i % 2 == 1 ? TitleOfCourtesy.Mr : TitleOfCourtesy.Ms,
{
ID = i,
CompanyName = companyNames[i % 6],
HireDate = DateTime.Now.AddMonths(-i),
TitleOfCourtesy = i % 2 == 1 ? TitleOfCourtesy.Mr : TitleOfCourtesy.Ms,
WorkStatus = $"{(i < 20 ? "Office" : i >= 20 && i < 40 ? "Remote" : "Office, Remote")} {(i < 12 ? ", Temporary" : i >= 12 && i < 30 ? ", Permanent" : i > 30 && i < 44 ? ", Temporary" : ", Permanent")}"
}).AsQueryable());
employees = allEmployees;
}).AsQueryable());
}
private async Task WorkStatusFilterClear(RadzenDataGridColumn<Employee> column)
{
if (selectedWorkStatus != null)
if(selectedWorkStatus != null)
{
selectedWorkStatus = null;
await OnSelectedWorkStatusChange(column);
await OnSelectedWorkStatusChange(column);
}
await column.CloseFilter();
}
@@ -118,31 +116,28 @@ else
await column.CloseFilter();
}
bool initialSort = false;
async Task OnSelectedWorkStatusChange(RadzenDataGridColumn<Employee> column)
{
var createWhereExpression = new List<string>();
if (selectedWorkStatus?.Any() == true)
{
employees = await Task.FromResult(employees.Where(e =>
workStatusOperator == LogicalFilterOperator.Or ?
selectedWorkStatus.Any(s => e.WorkStatus.Contains(s)) :
selectedWorkStatus.All(s => e.WorkStatus.Contains(s))));
whereExpression = string.Join($" {Enum.GetName(typeof(LogicalFilterOperator), workStatusOperator).ToLowerInvariant()} ",
selectedWorkStatus.Select(s => $"it.WorkStatus.Contains(\"{s}\")"));
}
else
{
whereExpression = null;
selectedWorkStatus = null;
initialSort = true;
employees = allEmployees;
}
await column.SetCustomFilterExpressionAsync(whereExpression);
}
void OnRender(DataGridRenderEventArgs<Employee> args)
{
if (args.FirstRender || initialSort)
if (args.FirstRender)
{
initialSort = false;
args.Grid.Sorts.Add(new SortDescriptor() { Property = "CompanyName", SortOrder = SortOrder.Ascending });
args.Grid.Sorts.Add(new SortDescriptor() { Property = "TitleOfCourtesy", SortOrder = SortOrder.Ascending });
}