mirror of
https://github.com/radzenhq/radzen-blazor.git
synced 2026-07-08 18:16:08 +00:00
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:
244
.editorconfig
Normal file
244
.editorconfig
Normal 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
|
||||
288
Radzen.Blazor.Tests/ExpressionParserTests.cs
Normal file
288
Radzen.Blazor.Tests/ExpressionParserTests.cs
Normal 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] }));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}""]";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
417
Radzen.Blazor/ExpressionParser.cs
Normal file
417
Radzen.Blazor/ExpressionParser.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user