From c7db9394c852a59cd043d54c44621dee624309b8 Mon Sep 17 00:00:00 2001 From: Vladimir Enchev Date: Wed, 7 May 2025 13:11:30 +0300 Subject: [PATCH] V7 (#2144) * Remove Microsoft.CodeAnalysis. Add own C# expression parser. * Popup dialog animations (#2118) * Experiment with popup and dialog animations. * Move animations to a separate _animations.scss * Remove dialog closing animation. * Support side dialog positions. Prefix keyframes. * Use --rz-transition for animation function and duration * Reset site.css. * PanelMenu animations. * Display none is toggled a bit late. * RadzenPanel animations. * More animations. * Use transitions for panel menu. * Remove old code. * Accordion uses transitions. * Panel uses transitions. * Extract expand and collapse implementation in a separate component. * Set initial expand state earlier to prevent two renders. * Add open animation to notifications. * Handle onanimationend before toggling the animation classes. * Add menu animation. * Experiment with tree animation. * Add animations to fieldset. --------- Co-authored-by: yordanov * ExpressionSerializer and tests added (#2119) * Fix failing tests. * various components RequiresUnreferencedCode attribute added (#2120) * RequiresUnreferencedCode added to ExpressionSerializer * Update premium themes * RequiresUnreferencedCode added to ExpressionParser * FormComponentWithAutoComplete RequiresUnreferencedCode removed * Revert "FormComponentWithAutoComplete RequiresUnreferencedCode removed" This reverts commit ec900a4df863109d388c00b316f0d24092f087fc. * Revert "RequiresUnreferencedCode added to ExpressionParser" This reverts commit f93b3b159bab35258c6b07039952844de871fcb8. * Revert "RequiresUnreferencedCode added to ExpressionSerializer" This reverts commit 06fecec9a65416e969949eb49175857c530cb4a7. * Revert "various components RequiresUnreferencedCode attribute added (#2120)" This reverts commit 2ed1a6cac16f0867d89e0c9c2211486253445940. * Remove RadzenHtml. * ExpressionSerializer FormatValue updated to use InvariantCulture * Catch potential JS interop exceptions that could occur during disposing. * Revert "Remove RadzenHtml." This reverts commit 319085bf493fbba142610939c486d0f5de6c6a2e. * SelectBar made single tab stop * RadioButtonList and CheckBoxList made single tab stop * SelectBar accessibility improved * RadioButtonList accessibility improved * CheckBoxList accessibility improved * Update radio button focus styles * Update checkbox list focus styles * Update Checkbox Radio and SelectBar focus styles * SelectBar, CheckBoxList and RadioButtonList focus state improved * Check for Multiple added * Use non-rendering event handlers for transitionend. * Rename css class rz-selectbutton to rz-selectbar and improve focus states * Fix selectbar focus outline offset * Update premium themes * Selectbar item focus styles should not be visible if the item is disabled. * CheckBoxList and RadioButtonList item focus should be visible only on keyboard input * SelectBar, CheckBoxList and RadioButtonList focus logic improved * Update animations * RadzenText margin-block should be 0 if it is in RadzenStack. Resolves #2134 * RadzenText margin-block should be 0 if it is first level child in RadzenStack * CheckBoxList focused fixed * Add toggle state classes to panel menu icon. * Update accordion styles to reflect expander changes * Add animation styles to expand arrow in Menu and ProfileMenu * Use a instead of NavLink as it seems to cause performance issues. * Set @bind-Expanded. * Revert "Set @bind-Expanded." This reverts commit 994107367bdf09043950f8bbe701eb9edefec676. * Revert "Use a instead of NavLink as it seems to cause performance issues." This reverts commit 05d5bef8f421bbeb5828ba1e9c5af6793ea3d32a. * Reduce rendering of panel menu items. * Add panel menu component. * Use ChildContent to render the toggle icon of the panel menu item. * Sync panel menu item selection in the item itself. * Rename ExpandedInternal to expanded. * Move filtering to the panel menu component. * Remove the transitionend handler to avoid a second rendering pass. * Build the assets for the net9.0 framework. * Do not trigger render when Click is used. * Panel menu keyboard navigation renders only when needed. * Focus reworked to use AsNonRenderingEventHandler * Focus the first item. * Update Panel demo * Use a more robust algorithm for month view event rendering that handles overlapping of events across a week. * Use RadzenStack in RadioButtonList * Add parsing support for `&` and `|`. * Add parsing support for `^`, `>>` and `<<`. * Simplify expression parsing tests. * Use RadzenStack in RadioButtonList and CheckBoxList * Change defaults for AlignItems and JustifyContent in RadioButtonList and CheckBoxList * Update RadioButtonList and CheckBoxList demos * Add --rz-input-border-block-end css variables to improve Fluent theme styles * Removed AsNonRenderingEventHandler from RadioButtonList and HtmlEditor focus and blur * Removed AsNonRenderingEventHandler from CheckBoxList * Simplify RadzenTable rendering. * Optimize memory usage of the ClassList utility. * Refactor RadzenButton to use ClassList. * RadzenSelectBar and RadzenSplitButton use ClassList. * Refactor RadzenBadge and RadzenAlert to use ClassList. * Refactor RadzenCard and RadzenFormField to use ClassList. * Refactor RadzenCardGroup and progress components to use ClassList. * Refactor RadzenMenu to use ClassList. * Use ClassList in RadzenBody, RadzenLayout and editor rendering components. * RadzenDialog uses ClassList. * RadzenDataGrid uses ClassList. * RadzenPager uses ClassList. * RadzenColumn uses ClassList. * Fix RadzenSplitButtonItem focused state. --------- Co-authored-by: Atanas Korchev Co-authored-by: Atanas Korchev <454726+akorchev@users.noreply.github.com> Co-authored-by: yordanov Co-authored-by: Quentin H <67709967+quintushr@users.noreply.github.com> --- Radzen.Blazor.Tests/ExpressionParserTests.cs | 1945 +++++++++++++---- .../ExpressionSerializerTests.cs | 304 +++ Radzen.Blazor.Tests/FieldsetTests.cs | 10 +- Radzen.Blazor.Tests/PanelTests.cs | 10 +- Radzen.Blazor/DynamicTypeFactory.cs | 56 + Radzen.Blazor/ExpressionLexer.cs | 873 ++++++++ Radzen.Blazor/ExpressionParser.cs | 1352 +++++++----- Radzen.Blazor/ExpressionSerializer.cs | 462 ++++ Radzen.Blazor/GaugeBase.cs | 2 +- Radzen.Blazor/JSRuntimeExtensions.cs | 24 + Radzen.Blazor/NonRenderingExtensions.cs | 39 + Radzen.Blazor/QueryableExtension.cs | 687 +----- Radzen.Blazor/Radzen.Blazor.csproj | 9 +- Radzen.Blazor/RadzenAccordion.razor | 23 +- Radzen.Blazor/RadzenAccordion.razor.cs | 6 + Radzen.Blazor/RadzenAlert.razor.cs | 44 +- Radzen.Blazor/RadzenAutoComplete.razor | 4 +- Radzen.Blazor/RadzenAutoComplete.razor.cs | 14 +- Radzen.Blazor/RadzenBadge.razor.cs | 26 +- Radzen.Blazor/RadzenBody.razor.cs | 10 +- Radzen.Blazor/RadzenButton.razor.cs | 18 +- Radzen.Blazor/RadzenCard.razor.cs | 16 +- Radzen.Blazor/RadzenCardGroup.razor.cs | 20 +- Radzen.Blazor/RadzenCarouselItem.razor | 2 +- Radzen.Blazor/RadzenCarouselItem.razor.cs | 7 +- Radzen.Blazor/RadzenChart.razor.cs | 2 +- Radzen.Blazor/RadzenCheckBox.razor | 14 +- Radzen.Blazor/RadzenCheckBox.razor.cs | 19 +- Radzen.Blazor/RadzenCheckBoxList.razor | 14 +- Radzen.Blazor/RadzenCheckBoxList.razor.cs | 142 +- Radzen.Blazor/RadzenColumn.razor.cs | 162 +- Radzen.Blazor/RadzenComponent.cs | 6 +- Radzen.Blazor/RadzenContextMenu.razor.cs | 2 +- Radzen.Blazor/RadzenDataGrid.razor.cs | 35 +- Radzen.Blazor/RadzenDatePicker.razor.cs | 4 +- Radzen.Blazor/RadzenDialog.razor | 32 +- Radzen.Blazor/RadzenDropDown.razor | 2 +- Radzen.Blazor/RadzenDropDown.razor.cs | 4 +- Radzen.Blazor/RadzenDropDownDataGrid.razor | 2 +- Radzen.Blazor/RadzenDropDownDataGrid.razor.cs | 4 +- Radzen.Blazor/RadzenFieldset.razor | 18 +- Radzen.Blazor/RadzenFieldset.razor.cs | 25 +- Radzen.Blazor/RadzenFileInput.razor | 4 +- Radzen.Blazor/RadzenFileInput.razor.cs | 23 +- Radzen.Blazor/RadzenFormField.razor.cs | 11 +- Radzen.Blazor/RadzenGoogleMap.razor.cs | 2 +- Radzen.Blazor/RadzenHtmlEditor.razor.cs | 2 +- Radzen.Blazor/RadzenLayout.cs | 14 +- Radzen.Blazor/RadzenMenu.razor | 2 +- Radzen.Blazor/RadzenMenu.razor.cs | 34 +- Radzen.Blazor/RadzenNotificationMessage.razor | 2 +- Radzen.Blazor/RadzenPager.razor | 2 +- Radzen.Blazor/RadzenPager.razor.cs | 26 +- Radzen.Blazor/RadzenPanel.razor | 11 +- Radzen.Blazor/RadzenPanel.razor.cs | 29 +- Radzen.Blazor/RadzenPanelMenu.razor | 10 +- Radzen.Blazor/RadzenPanelMenu.razor.cs | 144 +- Radzen.Blazor/RadzenPanelMenuItem.razor | 30 +- Radzen.Blazor/RadzenPanelMenuItem.razor.cs | 271 ++- Radzen.Blazor/RadzenProfileMenu.razor | 2 +- Radzen.Blazor/RadzenProfileMenu.razor.cs | 17 +- Radzen.Blazor/RadzenProgressBar.razor.cs | 31 +- .../RadzenProgressBarCircular.razor.cs | 59 +- Radzen.Blazor/RadzenRadioButtonList.razor | 14 +- Radzen.Blazor/RadzenRadioButtonList.razor.cs | 143 +- Radzen.Blazor/RadzenScheduler.razor.cs | 2 +- Radzen.Blazor/RadzenSecurityCode.razor.cs | 2 +- Radzen.Blazor/RadzenSelectBar.razor | 8 +- Radzen.Blazor/RadzenSelectBar.razor.cs | 115 +- Radzen.Blazor/RadzenSlider.razor.cs | 2 +- Radzen.Blazor/RadzenSplitButton.razor | 4 +- Radzen.Blazor/RadzenSplitButton.razor.cs | 34 +- Radzen.Blazor/RadzenSplitButtonItem.razor | 2 +- Radzen.Blazor/RadzenSplitButtonItem.razor.cs | 5 +- Radzen.Blazor/RadzenTable.razor | 8 +- Radzen.Blazor/RadzenTable.razor.cs | 81 +- Radzen.Blazor/RadzenTableBody.razor | 8 +- Radzen.Blazor/RadzenTableBody.razor.cs | 44 +- Radzen.Blazor/RadzenTableCell.razor | 8 +- Radzen.Blazor/RadzenTableCell.razor.cs | 46 +- Radzen.Blazor/RadzenTableHeader.razor | 8 +- Radzen.Blazor/RadzenTableHeader.razor.cs | 43 +- Radzen.Blazor/RadzenTableHeaderCell.razor | 8 +- Radzen.Blazor/RadzenTableHeaderCell.razor.cs | 39 +- Radzen.Blazor/RadzenTableHeaderRow.razor | 8 +- Radzen.Blazor/RadzenTableHeaderRow.razor.cs | 67 +- Radzen.Blazor/RadzenTableRow.razor | 8 +- Radzen.Blazor/RadzenTableRow.razor.cs | 75 +- Radzen.Blazor/RadzenTabsItem.razor | 2 +- Radzen.Blazor/RadzenTabsItem.razor.cs | 11 +- Radzen.Blazor/RadzenText.cs | 9 +- Radzen.Blazor/RadzenToggleButton.razor.cs | 36 +- Radzen.Blazor/RadzenTreeItem.razor | 12 +- Radzen.Blazor/RadzenTreeItem.razor.cs | 29 +- Radzen.Blazor/RadzenUpload.razor.cs | 10 +- Radzen.Blazor/Rendering/ClassList.cs | 273 ++- Radzen.Blazor/Rendering/DayView.razor | 10 +- Radzen.Blazor/Rendering/DialogContainer.razor | 40 +- Radzen.Blazor/Rendering/EditorButton.razor | 17 +- Radzen.Blazor/Rendering/EditorDropDown.razor | 17 +- .../Rendering/EditorDropDownItem.razor | 15 +- Radzen.Blazor/Rendering/Expander.razor | 5 + Radzen.Blazor/Rendering/Expander.razor.cs | 65 + Radzen.Blazor/Rendering/Legend.razor | 15 +- Radzen.Blazor/Rendering/MonthView.razor | 267 ++- Radzen.Blazor/Rendering/StringBuilderCache.cs | 59 + Radzen.Blazor/Rendering/WeekView.razor | 10 +- Radzen.Blazor/Rendering/YearPlannerView.razor | 8 +- .../Rendering/YearTimelineView.razor | 7 +- Radzen.Blazor/Rendering/YearView.razor | 7 - Radzen.Blazor/themes/_components.scss | 4 +- Radzen.Blazor/themes/_css-variables.scss | 12 + .../themes/components/blazor/_accordion.scss | 31 +- .../themes/components/blazor/_animations.scss | 25 + .../themes/components/blazor/_checkbox.scss | 53 +- .../themes/components/blazor/_datafilter.scss | 2 +- .../themes/components/blazor/_dialog.scss | 141 +- .../themes/components/blazor/_expander.scss | 18 + .../themes/components/blazor/_form-field.scss | 15 +- .../themes/components/blazor/_input.scss | 14 + .../themes/components/blazor/_menu.scss | 29 +- .../themes/components/blazor/_panel-menu.scss | 12 +- .../themes/components/blazor/_panel.scss | 6 - .../components/blazor/_profile-menu.scss | 10 + .../themes/components/blazor/_radio.scss | 56 +- .../themes/components/blazor/_selectbar.scss | 112 +- .../themes/components/blazor/_typography.scss | 5 + Radzen.Blazor/themes/material-base.scss | 2 +- Radzen.Blazor/themes/material-dark-base.scss | 2 +- Radzen.Blazor/themes/material-dark.scss | 2 +- Radzen.Blazor/themes/material.scss | 2 +- Radzen.Blazor/wwwroot/Radzen.Blazor.js | 28 +- .../Pages/CheckBoxListOrientation.razor | 39 +- .../Pages/CheckBoxListPage.razor | 5 +- RadzenBlazorDemos/Pages/Index.razor | 4 +- RadzenBlazorDemos/Pages/PanelConfig.razor | 4 +- .../Pages/RadioButtonListOrientation.razor | 39 +- .../Pages/RadioButtonListPage.razor | 5 +- RadzenBlazorDemos/Services/ExampleService.cs | 6 +- RadzenBlazorDemos/Shared/MainLayout.razor | 43 +- RadzenBlazorDemos/Shared/NavigationItem.razor | 23 +- RadzenBlazorDemos/Shared/PanelMenu.razor | 52 + RadzenBlazorDemos/wwwroot/css/fluent-base.css | 2 +- .../wwwroot/css/fluent-dark-base.css | 2 +- .../wwwroot/css/material3-base.css | 2 +- .../wwwroot/css/material3-dark-base.css | 2 +- 146 files changed, 6414 insertions(+), 3277 deletions(-) create mode 100644 Radzen.Blazor.Tests/ExpressionSerializerTests.cs create mode 100644 Radzen.Blazor/DynamicTypeFactory.cs create mode 100644 Radzen.Blazor/ExpressionLexer.cs create mode 100644 Radzen.Blazor/ExpressionSerializer.cs create mode 100644 Radzen.Blazor/JSRuntimeExtensions.cs create mode 100644 Radzen.Blazor/NonRenderingExtensions.cs create mode 100644 Radzen.Blazor/Rendering/Expander.razor create mode 100644 Radzen.Blazor/Rendering/Expander.razor.cs create mode 100644 Radzen.Blazor/Rendering/StringBuilderCache.cs create mode 100644 Radzen.Blazor/themes/components/blazor/_animations.scss create mode 100644 Radzen.Blazor/themes/components/blazor/_expander.scss create mode 100644 RadzenBlazorDemos/Shared/PanelMenu.razor diff --git a/Radzen.Blazor.Tests/ExpressionParserTests.cs b/Radzen.Blazor.Tests/ExpressionParserTests.cs index 3be5d94ad..fe0fe4b0d 100644 --- a/Radzen.Blazor.Tests/ExpressionParserTests.cs +++ b/Radzen.Blazor.Tests/ExpressionParserTests.cs @@ -1,423 +1,1538 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; +using System.Runtime.CompilerServices; using System.Linq.Dynamic.Core; using Xunit; -namespace Radzen.Blazor.Tests +#nullable enable + +namespace Radzen.Blazor.Tests; + +public enum CarType { - 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 WorkStatuses { get; set; } - public List Statuses { get; set; } - } - - class Category - { - public string CategoryName { get; set; } - - } - - class Order - { - public DateTime OrderDate { get; set; } - } - - class Product - { - public string ProductName { get; set; } - - public Category Category { get; set; } - } - - [Fact] - public void Should_ParseBindaryExpression() - { - var expression = ExpressionParser.ParsePredicate("p => p.Name == \"foo\""); - - var func = expression.Compile(); - - Assert.True(func(new Person() { Name = "foo" })); - } - - [Fact] - public void Should_ParseConditionalExpression() - { - var expression = ExpressionParser.ParsePredicate("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("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("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 - { - public T Value { get; set; } - } - - [Fact] - public void Should_SupportDateOnlyParsing() - { - var expression = ExpressionParser.ParsePredicate>("it => it.Value >= DateOnly.Parse(\"2025-02-11\")"); - - var func = expression.Compile(); - - Assert.True(func(new ItemWithGenericProperty { Value = new DateOnly(2025, 2, 11) })); - } - - [Fact] - public void Should_SupportTimeOnlyParsing() - { - var expression = ExpressionParser.ParsePredicate>("it => it.Value >= TimeOnly.Parse(\"12:00:00\")"); - var func = expression.Compile(); - Assert.True(func(new ItemWithGenericProperty { Value = new TimeOnly(12, 0, 0) })); - } - - [Fact] - public void Should_SupportGuidParsing() - { - var expression = ExpressionParser.ParsePredicate>("it => it.Value == Guid.Parse(\"f0e7e7d8-4f4d-4b5f-8b3e-3f1d1b4f5f5f\")"); - var func = expression.Compile(); - Assert.True(func(new ItemWithGenericProperty { Value = Guid.Parse("f0e7e7d8-4f4d-4b5f-8b3e-3f1d1b4f5f5f") })); - } - - [Fact] - public void Should_SupportDateTimeOffsetParsing() - { - var expression = ExpressionParser.ParsePredicate>("it => it.Value == DateTimeOffset.Parse(\"2025-02-11\")"); - var func = expression.Compile(); - Assert.True(func(new ItemWithGenericProperty { Value = DateTimeOffset.Parse("2025-02-11") })); - } - - [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>("it => it.Value.Any(i => (new []{0}).Contains(i))", typeLocator); - - var func = expression.Compile(); - - Assert.True(func(new ItemWithGenericProperty { Value = [CarType.Sedan] })); - } - - [Fact] - public void Should_SupportNullableCollection() - { - var expression = ExpressionParser.ParsePredicate("it => new bool?[]{ false }.Contains(it.Famous)"); - var func = expression.Compile(); - - Assert.True(func(new Person { Famous = false })); - } - - [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($"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("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("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_SupportNestedLambdasWithNot() - { - var expression = ExpressionParser.ParsePredicate("it => it.WorkStatuses.Any(i => !(new []{\"Office\"}).Contains(i.Name))"); - var func = expression.Compile(); - - Assert.False(func(new OrderDetail { WorkStatuses = [new() { Name = "Office" }] })); - } - - [Fact] - public void Should_SupportNestedLambdasWithEnums() - { - var expression = ExpressionParser.ParsePredicate("it => it.Statuses.Any(i => (new []{1}).Contains(i))"); - var func = expression.Compile(); - - Assert.True(func(new OrderDetail { Statuses = new List() { (Status)1 } })); - } - - [Fact] - public void Should_SupportNestedLambdasWithComplexMethod() - { - var expression = ExpressionParser.ParsePredicate("it => new [] { (Status)1 }.Intersect(it.Statuses).Any()", type => typeof(Status)); - var func = expression.Compile(); - - Assert.True(func(new OrderDetail { Statuses = new List() { (Status)1 } })); - } - - [Fact] - public void Should_SupportToLower() - { - var expression = ExpressionParser.ParsePredicate("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("it => it.Age == 50"); - - var func = expression.Compile(); - - Assert.True(func(new Person { Age = 50 })); - } - - [Fact] - public void Should_SupportNullablePropertiesWithArray() - { - var expression = ExpressionParser.ParsePredicate("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("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>("it => it.Value == 50"); - - var func = expression.Compile(); - - Assert.True(func(new ItemWithGenericProperty { Value = 50.0 })); - } - - [Fact] - public void Should_SupportNullCoalescence() - { - var expression = ExpressionParser.ParsePredicate>("it => (it.Value ?? 0) == 0"); - - var func = expression.Compile(); - - Assert.True(func(new ItemWithGenericProperty { Value = null })); - } - - [Fact] - public void Should_SupportNullConditionAndCoalescence() - { - var expression = ExpressionParser.ParsePredicate("it => (((it == null) ? null : it.Name) ?? \"\").Contains(\"Da\")"); - - var func = expression.Compile(); - - Assert.True(func(new Person { Name = "Dali" })); - } - - [Fact] - public void Should_CreateProjection() - { - var expression = ExpressionParser.ParseLambda("it => new { ProductName = it.Product.ProductName}"); - - var func = expression.Compile(); - - var result = func.DynamicInvoke(new OrderDetail { Product = new Product { ProductName = "Queso" } }); - - var property = result.GetType().GetProperty("ProductName"); - - Assert.Equal(typeof(string), property.PropertyType); - - Assert.Equal("Queso", property.GetValue(result)); - } - - [Fact] - public void Should_CreateProjectionFromNestedAccess() - { - var expression = ExpressionParser.ParseLambda("it => new { it.Product.Category.CategoryName }"); - - var func = expression.Compile(); - - var orderDetail = new OrderDetail { Product = new Product { Category = new Category { CategoryName = "Beverages" } } }; - - var x = new { orderDetail?.Product?.Category?.CategoryName }; - - var result = func.DynamicInvoke(orderDetail); - - var property = result.GetType().GetProperty("CategoryName"); - - Assert.Equal(typeof(string), property.PropertyType); - - Assert.Equal("Beverages", property.GetValue(result)); - } - - [Fact] - public void Should_CreateProjectionFromNestedConditionalAccess() - { - var expression = ExpressionParser.ParseLambda("it => new { it.Product?.Category?.CategoryName }"); - - var func = expression.Compile(); - - var orderDetail = new OrderDetail { Product = new Product { Category = new Category { CategoryName = "Beverages" } } }; - - var x = new { orderDetail?.Product?.Category?.CategoryName }; - - var result = func.DynamicInvoke(orderDetail); - - var property = result.GetType().GetProperty("CategoryName"); - - Assert.Equal(typeof(string), property.PropertyType); - - Assert.Equal("Beverages", property.GetValue(result)); - } - - [Fact] - public void Should_CreateProjectionFromConditionalAccess() - { - var expression = ExpressionParser.ParseLambda("it => new { ProductName = it.Product?.ProductName}"); - var func = expression.Compile(); - var result = func.DynamicInvoke(new OrderDetail { Product = null }); - - var property = result.GetType().GetProperty("ProductName"); - - Assert.Equal(typeof(string), property.PropertyType); - Assert.Null(property.GetValue(result)); - } - - [Fact] - public void Should_CreateProjectionFromNestedConditionalAccessAndAssignment() - { - var expression = ExpressionParser.ParseLambda("it => new { CategoryName = it.Product?.Category?.CategoryName}"); - var func = expression.Compile(); - var result = func.DynamicInvoke(new OrderDetail { Product = null }); - - var property = result.GetType().GetProperty("CategoryName"); - - Assert.Equal(typeof(string), property.PropertyType); - Assert.Null(property.GetValue(result)); - } - - [Fact] - public void Should_SelectByString() - { - var list = new List { 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_SelectByWithUntypedIQueryableString() - { - IQueryable list = new List { 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>("it => (int)it[\"foo\"] == 1"); - var func = expression.Compile(); - Assert.True(func(new Dictionary { ["foo"] = 1 })); - } - - [Fact] - public void Should_SupportDictionaryIndexAccessWithNullableCast() - { - var expression = ExpressionParser.ParsePredicate>("it => (Int32?)it[\"foo\"] == null"); - var func = expression.Compile(); - Assert.True(func(new Dictionary { ["foo"] = null })); - } - - [Fact] - public void Should_SupportArrayIndexAccess() - { - var expression = ExpressionParser.ParsePredicate>("it => it.Value[0] == 1"); - var func = expression.Compile(); - - Assert.True(func(new ItemWithGenericProperty { Value = [1] })); - } - } + 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? WorkStatuses { get; set; } + public List? Statuses { get; set; } + } + + class WorkStatus + { + public string? Name { get; set; } + } + + class Category + { + public string? CategoryName { get; set; } + } + + class Order + { + public DateTime OrderDate { get; set; } + } + + class Product + { + public string? ProductName { get; set; } + + public Category? Category { get; set; } + } + + [Fact] + public void Should_ParseStringLiteralExpression() + { + var expression = ExpressionParser.ParsePredicate("p => p.Name == \"foo\""); + + var func = expression.Compile(); + + Assert.True(func(new Person() { Name = "foo" })); + } + + [Fact] + public void Should_ThrowIfStringLiteralIsNotClosed() + { + Assert.Throws(() => ExpressionParser.ParsePredicate("p => p.Name == \"foo")); + } + + [Fact] + public void Should_ParseCharacterLiteral() + { + var expression = ExpressionParser.ParsePredicate>("p => p.Value == 'f'"); + + var func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = 'f' })); + } + + [Fact] + public void Should_ThrowExceptionsWhenThereAreMultipleCharacters() + { + Assert.Throws(() => ExpressionParser.ParsePredicate>("p => p.Value == 'foo'")); + } + + [Fact] + public void Should_ThrowIfCharacterLiteralIsNotClosed() + { + Assert.Throws(() => ExpressionParser.ParsePredicate>("p => p.Value == 'f")); + } + + [Fact] + public void Should_ReturnTargetType() + { + var expression = ExpressionParser.ParseLambda, char>("p => p.Value"); + + var func = expression.Compile(); + + Assert.Equal('\n', func(new ItemWithGenericProperty { Value = '\n' })); + } + + [Fact] + public void Should_ParseEscapeSequenceInCharacterLiteral() + { + var expression = ExpressionParser.ParsePredicate>("p => p.Value == '\\n'"); + + var func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = '\n' })); + } + + [Fact] + public void Should_ParseUnicodeEscapeSequenceInCharacterLiteral() + { + var expression = ExpressionParser.ParsePredicate>(@"p => p.Value == '\u000A'"); + + var func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = '\n' })); + + expression = ExpressionParser.ParsePredicate>(@"p => p.Value == '\x000A'"); + + func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = '\n' })); + + expression = ExpressionParser.ParsePredicate>(@"p => p.Value == '\x0A'"); + + func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = '\n' })); + + expression = ExpressionParser.ParsePredicate>(@"p => p.Value == '\x00A'"); + + func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = '\n' })); + + expression = ExpressionParser.ParsePredicate>(@"p => p.Value == '\U0000000a'"); + + func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = '\n' })); + } + + [Fact] + public void Should_ThrowIfUnicodeEscapeSequenceIsTooLong() + { + Assert.Throws(() => ExpressionParser.ParsePredicate>(@"p => p.Value == '\u00000'")); + } + + [Fact] + public void Should_ThrowIfUnicodeEscapeSequenceIsTooShort() + { + Assert.Throws(() => ExpressionParser.ParsePredicate>(@"p => p.Value == '\u0'")); + } + + [Fact] + public void Should_ParseUnicodeEscapeSequenceInStringLiteral() + { + var expression = ExpressionParser.ParsePredicate>(@"p => p.Value == ""\u000A"""); + + var func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = "\n" })); + + expression = ExpressionParser.ParsePredicate>(@"p => p.Value == ""\x000A"""); + + func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = "\n" })); + + expression = ExpressionParser.ParsePredicate>(@"p => p.Value == ""\x0A"""); + + func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = "\n" })); + + expression = ExpressionParser.ParsePredicate>(@"p => p.Value == ""\x00A"""); + + func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = "\n" })); + + expression = ExpressionParser.ParsePredicate>(@"p => p.Value == ""\U0000000a"""); + + func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = "\n" })); + } + + [Fact] + public void Should_ParseEscapeSequenceInStringLiteral() + { + var expression = ExpressionParser.ParsePredicate>(@"p => p.Value == ""\n"""); + + var func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = "\n" })); + } + + [Fact] + public void Should_ParseNotEqualsExpression() + { + var expression = ExpressionParser.ParsePredicate("p => p.Name != \"foo\""); + + var func = expression.Compile(); + + Assert.True(func(new Person() { Name = "bar" })); + Assert.False(func(new Person() { Name = "foo" })); + } + + [Fact] + public void Should_ParseGreaterThanExpression() + { + var expression = ExpressionParser.ParsePredicate("p => p.Age > 30"); + + var func = expression.Compile(); + + Assert.True(func(new Person() { Age = 31 })); + Assert.False(func(new Person() { Age = 30 })); + Assert.False(func(new Person() { Age = 29 })); + } + + [Fact] + public void Should_ParseLessThanExpression() + { + var expression = ExpressionParser.ParsePredicate("p => p.Age < 30"); + + var func = expression.Compile(); + + Assert.True(func(new Person() { Age = 29 })); + Assert.False(func(new Person() { Age = 30 })); + Assert.False(func(new Person() { Age = 31 })); + } + + [Fact] + public void Should_ParseLessThanOrEqualExpression() + { + var expression = ExpressionParser.ParsePredicate("p => p.Age <= 30"); + + var func = expression.Compile(); + + Assert.True(func(new Person() { Age = 29 })); + Assert.True(func(new Person() { Age = 30 })); + Assert.False(func(new Person() { Age = 31 })); + } + + [Fact] + public void Should_ParseGreaterThanOrEqualExpression() + { + var expression = ExpressionParser.ParsePredicate("p => p.Age >= 30"); + + var func = expression.Compile(); + + Assert.True(func(new Person() { Age = 31 })); + Assert.True(func(new Person() { Age = 30 })); + Assert.False(func(new Person() { Age = 29 })); + } + + [Fact] + public void Should_ParseExpressionWithParentheses() + { + var expression = ExpressionParser.ParsePredicate("p => (p.Name == \"foo\")"); + + var func = expression.Compile(); + + Assert.True(func(new Person() { Name = "foo" })); + } + + [Fact] + public void Should_ThrowIfParenthesesAreNotClosed() + { + Assert.Throws(() => ExpressionParser.ParsePredicate("p => (p.Name == \"foo\"")); + } + + [Fact] + public void Should_ParseLogicalOperations() + { + var expression = ExpressionParser.ParsePredicate("p => (p.Name == \"foo\" || p.Name == \"bar\") && p.Age == 50"); + + var func = expression.Compile(); + + Assert.True(func(new Person() { Name = "foo", Age = 50 })); + Assert.True(func(new Person() { Name = "bar", Age = 50 })); + Assert.False(func(new Person() { Name = "baz", Age = 50 })); + Assert.False(func(new Person() { Name = "foo", Age = 51 })); + } + + [Fact] + public void Should_ParseFractionNumbersAsDouble() + { + var expression = ExpressionParser.ParsePredicate>("p => p.Value == 50.5"); + + var func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = 50.5 })); + } + + [Fact] + public void Should_ParseNegativeNumbers() + { + var expression = ExpressionParser.ParsePredicate>("p => p.Value == -50.5"); + + var func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = -50.5 })); + } + + [Fact] + public void Should_ParsePositiveNumbers() + { + var expression = ExpressionParser.ParsePredicate>("p => p.Value == +50.5"); + + var func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = +50.5 })); + } + + [Fact] + public void Should_ParseFloatLiteral() + { + var expression = ExpressionParser.ParsePredicate>("p => p.Value == 50.5f"); + + var func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = 50.5f })); + } + + [Fact] + public void Should_ParseDoubleLiteral() + { + var expression = ExpressionParser.ParsePredicate>("p => p.Value == 50.5d"); + + var func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = 50.5 })); + } + + [Fact] + public void Should_ParseDecimalLiteral() + { + var expression = ExpressionParser.ParsePredicate>("p => p.Value == 50.5m"); + + var func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = 50.5m })); + } + + [Fact] + public void Should_ParseLongLiteral() + { + var expression = ExpressionParser.ParsePredicate>("p => p.Value == 50L"); + + var func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = 50L })); + } + + class ItemWithGenericProperty + { + public T Value { get; set; } = default!; + } + + [Fact] + public void Should_ParseAdditionExpression() + { + var expression = ExpressionParser.ParseLambda, int>("p => p.Value + 5"); + + var func = expression.Compile(); + + Assert.Equal(26 + 5, func(new ItemWithGenericProperty { Value = 26 })); + } + + [Fact] + public void Should_ParseBinaryAndExpressionWithAddition() + { + var expression = ExpressionParser.ParseLambda, int>("p => p.Value + 5 & 1"); + var func = expression.Compile(); + + Assert.Equal(25 + 5 & 1, func(new ItemWithGenericProperty { Value = 25 })); + } + + [Fact] + public void Should_ParseBinaryAndExpressionWithMultiplication() + { + var expression = ExpressionParser.ParseLambda, int>("p => p.Value * 5 & 1"); + var func = expression.Compile(); + + Assert.Equal(25 * 5 & 1, func(new ItemWithGenericProperty { Value = 25 })); + } + + [Fact] + public void Should_ParseBinaryOrExpressionWithAddition() + { + var expression = ExpressionParser.ParseLambda, int>("p => p.Value + 5 | 1"); + var func = expression.Compile(); + + Assert.Equal(25 + 5 | 1, func(new ItemWithGenericProperty { Value = 25 })); + } + + [Fact] + public void Should_ParseBinaryOrExpressionWithMultiplication() + { + var expression = ExpressionParser.ParseLambda, int>("p => p.Value * 5 | 1"); + var func = expression.Compile(); + + Assert.Equal(25 * 5 | 1, func(new ItemWithGenericProperty { Value = 25 })); + } + + [Fact] + public void Should_ParseBinaryXorExpressionWithAddition() + { + var expression = ExpressionParser.ParseLambda, int>("p => p.Value + 5 ^ 1"); + var func = expression.Compile(); + + Assert.Equal(25 + 5 ^ 1, func(new ItemWithGenericProperty { Value = 25 })); + } + + [Fact] + public void Should_ParseBinaryXorExpressionWithMultiplication() + { + var expression = ExpressionParser.ParseLambda, int>("p => p.Value * 5 ^ 1"); + var func = expression.Compile(); + + Assert.Equal(25 * 5 ^ 1, func(new ItemWithGenericProperty { Value = 25 })); + } + + [Fact] + public void Should_ParseLeftShiftExpressionWithAddition() + { + var expression = ExpressionParser.ParseLambda, int>("p => (p.Value + 5) << 1"); + var func = expression.Compile(); + + Assert.Equal((25 + 5) << 1, func(new ItemWithGenericProperty { Value = 25 })); + } + + [Fact] + public void Should_ParseLeftShiftExpressionWithMultiplication() + { + var expression = ExpressionParser.ParseLambda, int>("p => (p.Value * 5) << 1"); + var func = expression.Compile(); + + Assert.Equal((25 * 5) << 1, func(new ItemWithGenericProperty { Value = 25 })); + } + + [Fact] + public void Should_ParseRightShiftExpressionWithAddition() + { + var expression = ExpressionParser.ParseLambda, int>("p => (p.Value + 5) >> 1"); + var func = expression.Compile(); + + Assert.Equal((25 + 5) >> 1, func(new ItemWithGenericProperty { Value = 25 })); + } + + [Fact] + public void Should_ParseRightShiftExpressionWithMultiplication() + { + var expression = ExpressionParser.ParseLambda, int>("p => (p.Value * 5) >> 1"); + var func = expression.Compile(); + + Assert.Equal((25 * 5) >> 1, func(new ItemWithGenericProperty { Value = 25 })); + } + + [Fact] + public void Should_ParseMultipleAdditions() + { + var expression = ExpressionParser.ParseLambda, int>("p => p.Value + 5 + 10"); + + var func = expression.Compile(); + + Assert.Equal(16 + 5 + 10, func(new ItemWithGenericProperty { Value = 16 })); + } + + [Fact] + public void Should_ParseSubtractionExpression() + { + var expression = ExpressionParser.ParseLambda, int>("p => p.Value - 5"); + + var func = expression.Compile(); + + Assert.Equal(36 - 5, func(new ItemWithGenericProperty { Value = 36 })); + } + + [Fact] + public void Should_ParseMixedAdditionAndSubtraction() + { + var expression = ExpressionParser.ParseLambda, int>("p => p.Value + 10 - 5"); + + var func = expression.Compile(); + + Assert.Equal(26 + 10 - 5, func(new ItemWithGenericProperty { Value = 26 })); + } + + [Fact] + public void Should_ParseMultiplicationExpression() + { + var expression = ExpressionParser.ParseLambda, int>("p => p.Value * 2"); + + var func = expression.Compile(); + + Assert.Equal(16 * 2, func(new ItemWithGenericProperty { Value = 16 })); + } + + [Fact] + public void Should_RespectOperatorPrecedence() + { + var expression = ExpressionParser.ParseLambda, int>("p => p.Value * 2 + 5"); + + var func = expression.Compile(); + + Assert.Equal(13 * 2 + 5, func(new ItemWithGenericProperty { Value = 13 })); + + var expression2 = ExpressionParser.ParseLambda, int>("p => p.Value * (2 + 5)"); + + var func2 = expression2.Compile(); + + Assert.Equal(5 * (2 + 5), func2(new ItemWithGenericProperty { Value = 5 })); + } + + [Fact] + public void Should_ParseDivisionExpression() + { + var expression = ExpressionParser.ParseLambda, int>("p => p.Value / 2"); + + var func = expression.Compile(); + + Assert.Equal(32 / 2, func(new ItemWithGenericProperty { Value = 32 })); + } + + [Fact] + public void Should_ParseMixedMultiplicationAndDivision() + { + var expression = ExpressionParser.ParseLambda, int>("p => p.Value * 2 / 4"); + + var func = expression.Compile(); + + Assert.Equal(32 * 2 / 4, func(new ItemWithGenericProperty { Value = 32 })); + + var expression2 = ExpressionParser.ParseLambda, int>("p => p.Value * (8 / 4)"); + + var func2 = expression2.Compile(); + + Assert.Equal(8 * (8 / 4), func2(new ItemWithGenericProperty { Value = 8 })); + } + + [Fact] + public void Should_ParseTernaryExpression() + { + var expression = ExpressionParser.ParseLambda("p => p.Age > 30 ? \"foo\" : \"bar\""); + + var func = expression.Compile(); + + Assert.Equal("foo", func(new Person() { Age = 31 })); + Assert.Equal("bar", func(new Person() { Age = 29 })); + } + + [Fact] + public void Should_ParseNestedTernaryExpression() + { + var expression = ExpressionParser.ParseLambda("p => p.Age > 30 ? \"foo\" : (p.Age < 20 ? \"bar\" : \"baz\")"); + + var func = expression.Compile(); + + Assert.Equal("foo", func(new Person() { Age = 31 })); + Assert.Equal("bar", func(new Person() { Age = 19 })); + Assert.Equal("baz", func(new Person() { Age = 25 })); + } + + [Fact] + public void Should_ParseConditionalAccessExpression() + { + var expression = ExpressionParser.ParsePredicate("it => it.Product?.ProductName == \"foo\""); + + var func = expression.Compile(); + + Assert.True(func(new OrderDetail() { Product = new Product { ProductName = "foo" } })); + Assert.False(func(new OrderDetail() { Product = new Product { ProductName = "bar" } })); + Assert.False(func(new OrderDetail() { Product = null })); + } + + [Fact] + public void Should_ParseNullLiteral() + { + var expression = ExpressionParser.ParsePredicate("it => it.Product == null"); + + var func = expression.Compile(); + + Assert.True(func(new OrderDetail() { Product = null })); + Assert.False(func(new OrderDetail() { Product = new Product() })); + } + + [Fact] + public void Should_ParseTrueLiteral() + { + var expression = ExpressionParser.ParsePredicate("it => it.Famous == true"); + + var func = expression.Compile(); + + Assert.True(func(new Person() { Famous = true })); + Assert.False(func(new Person() { Famous = false })); + } + + [Fact] + public void Should_ParseFalseLiteral() + { + var expression = ExpressionParser.ParsePredicate("it => it.Famous == false"); + + var func = expression.Compile(); + + Assert.True(func(new Person() { Famous = false })); + Assert.False(func(new Person() { Famous = true })); + } + + [Fact] + public void Support_MethodInvocation() + { + var expression = ExpressionParser.ParsePredicate("it => it.Product.ProductName.Contains(\"foo\")"); + + var func = expression.Compile(); + + Assert.True(func(new OrderDetail() { Product = new Product { ProductName = "foo" } })); + Assert.False(func(new OrderDetail() { Product = new Product { ProductName = "bar" } })); + } + + [Fact] + public void Should_ParseConditionalMethodInvocation() + { + var expression = ExpressionParser.ParsePredicate("it => it.Product?.ProductName?.ToLower() == \"foo\""); + + var func = expression.Compile(); + + Assert.True(func(new OrderDetail() { Product = new Product { ProductName = "FOO" } })); + Assert.False(func(new OrderDetail() { Product = new Product { ProductName = "BAR" } })); + Assert.False(func(new OrderDetail() { Product = null })); + } + + [Fact] + public void Should_ParseConditionalExpression() + { + var expression = ExpressionParser.ParsePredicate("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("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("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) } })); + } + + [Fact] + public void Should_SupportDateTimeParsingWithCultureInfo() + { + var expression = ExpressionParser.ParsePredicate("it => it.Order.OrderDate >= DateTime.Parse(\"2025-02-11\", CultureInfo.InvariantCulture)"); + + var func = expression.Compile(); + Assert.True(func(new OrderDetail { Order = new Order { OrderDate = new DateTime(2025, 2, 11) } })); + } + + [Fact] + public void Should_SupportDateOnlyParsing() + { + var expression = ExpressionParser.ParsePredicate>("it => it.Value >= DateOnly.Parse(\"2025-02-11\")"); + + var func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = new DateOnly(2025, 2, 11) })); + } + + [Fact] + public void Should_SupportTimeOnlyParsing() + { + var expression = ExpressionParser.ParsePredicate>("it => it.Value >= TimeOnly.Parse(\"12:00:00\")"); + var func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = new TimeOnly(12, 0, 0) })); + } + + [Fact] + public void Should_SupportGuidParsing() + { + var expression = ExpressionParser.ParsePredicate>("it => it.Value == Guid.Parse(\"f0e7e7d8-4f4d-4b5f-8b3e-3f1d1b4f5f5f\")"); + var func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = Guid.Parse("f0e7e7d8-4f4d-4b5f-8b3e-3f1d1b4f5f5f") })); + } + + [Fact] + public void Should_SupportDateTimeOffsetParsing() + { + var expression = ExpressionParser.ParsePredicate>("it => it.Value == DateTimeOffset.Parse(\"2025-02-11\")"); + var func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = DateTimeOffset.Parse("2025-02-11") })); + } + + [Fact] + public void Should_SupportIntStaticMembers() + { + var expression = ExpressionParser.ParsePredicate>("it => it.Value == int.MaxValue"); + var func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = int.MaxValue })); + Assert.False(func(new ItemWithGenericProperty { Value = int.MinValue })); + + expression = ExpressionParser.ParsePredicate>("it => it.Value == int.MinValue"); + func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = int.MinValue })); + Assert.False(func(new ItemWithGenericProperty { Value = int.MaxValue })); + } + + [Fact] + public void Should_SupportDecimalStaticMembers() + { + var expression = ExpressionParser.ParsePredicate>("it => it.Value == decimal.MaxValue"); + var func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = decimal.MaxValue })); + Assert.False(func(new ItemWithGenericProperty { Value = decimal.MinValue })); + + expression = ExpressionParser.ParsePredicate>("it => it.Value == decimal.MinValue"); + func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = decimal.MinValue })); + Assert.False(func(new ItemWithGenericProperty { Value = decimal.MaxValue })); + } + + [Fact] + public void Should_SupportCharStaticMethods() + { + var expression = ExpressionParser.ParsePredicate>("it => char.IsLetter(it.Value)"); + var func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = 'A' })); + Assert.False(func(new ItemWithGenericProperty { Value = '1' })); + + expression = ExpressionParser.ParsePredicate>("it => char.IsDigit(it.Value)"); + func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = '1' })); + Assert.False(func(new ItemWithGenericProperty { Value = 'A' })); + } + + [Fact] + public void Should_SupportDoubleStaticMembers() + { + var expression = ExpressionParser.ParsePredicate>("it => it.Value == double.MaxValue"); + var func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = double.MaxValue })); + Assert.False(func(new ItemWithGenericProperty { Value = double.MinValue })); + + expression = ExpressionParser.ParsePredicate>("it => it.Value == double.MinValue"); + func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = double.MinValue })); + Assert.False(func(new ItemWithGenericProperty { Value = double.MaxValue })); + + expression = ExpressionParser.ParsePredicate>("it => double.IsNaN(it.Value)"); + func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = double.NaN })); + Assert.False(func(new ItemWithGenericProperty { Value = 0.0 })); + } + + [Fact] + public void Should_SupportFloatStaticMembers() + { + var expression = ExpressionParser.ParsePredicate>("it => it.Value == float.MaxValue"); + var func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = float.MaxValue })); + Assert.False(func(new ItemWithGenericProperty { Value = float.MinValue })); + + expression = ExpressionParser.ParsePredicate>("it => it.Value == float.MinValue"); + func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = float.MinValue })); + Assert.False(func(new ItemWithGenericProperty { Value = float.MaxValue })); + + expression = ExpressionParser.ParsePredicate>("it => float.IsNaN(it.Value)"); + func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = float.NaN })); + Assert.False(func(new ItemWithGenericProperty { Value = 0.0f })); + } + + [Fact] + public void Should_SupportStringStaticMethods() + { + var expression = ExpressionParser.ParsePredicate>("it => string.IsNullOrEmpty(it.Value)"); + var func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = null })); + Assert.True(func(new ItemWithGenericProperty { Value = "" })); + Assert.False(func(new ItemWithGenericProperty { Value = "test" })); + + expression = ExpressionParser.ParsePredicate>("it => string.IsNullOrWhiteSpace(it.Value)"); + func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = null })); + Assert.True(func(new ItemWithGenericProperty { Value = " " })); + Assert.False(func(new ItemWithGenericProperty { Value = "test" })); + } + + [Fact] + public void Should_SupportLongStaticMembers() + { + var expression = ExpressionParser.ParsePredicate>("it => it.Value == long.MaxValue"); + var func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = long.MaxValue })); + Assert.False(func(new ItemWithGenericProperty { Value = long.MinValue })); + + expression = ExpressionParser.ParsePredicate>("it => it.Value == long.MinValue"); + func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = long.MinValue })); + Assert.False(func(new ItemWithGenericProperty { Value = long.MaxValue })); + } + + [Fact] + public void Should_SupportShortStaticMembers() + { + var expression = ExpressionParser.ParsePredicate>("it => it.Value == short.MaxValue"); + var func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = short.MaxValue })); + Assert.False(func(new ItemWithGenericProperty { Value = short.MinValue })); + + expression = ExpressionParser.ParsePredicate>("it => it.Value == short.MinValue"); + func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = short.MinValue })); + Assert.False(func(new ItemWithGenericProperty { Value = short.MaxValue })); + } + + [Fact] + public void Should_SupportByteStaticMembers() + { + var expression = ExpressionParser.ParsePredicate>("it => it.Value == byte.MaxValue"); + var func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = byte.MaxValue })); + Assert.False(func(new ItemWithGenericProperty { Value = byte.MinValue })); + + expression = ExpressionParser.ParsePredicate>("it => it.Value == byte.MinValue"); + func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = byte.MinValue })); + Assert.False(func(new ItemWithGenericProperty { Value = byte.MaxValue })); + } + + [Fact] + public void Should_SupportSByteStaticMembers() + { + var expression = ExpressionParser.ParsePredicate>("it => it.Value == sbyte.MaxValue"); + var func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = sbyte.MaxValue })); + Assert.False(func(new ItemWithGenericProperty { Value = sbyte.MinValue })); + + expression = ExpressionParser.ParsePredicate>("it => it.Value == sbyte.MinValue"); + func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = sbyte.MinValue })); + Assert.False(func(new ItemWithGenericProperty { Value = sbyte.MaxValue })); + } + + [Fact] + public void Should_SupportUIntStaticMembers() + { + var expression = ExpressionParser.ParsePredicate>("it => it.Value == uint.MaxValue"); + var func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = uint.MaxValue })); + Assert.False(func(new ItemWithGenericProperty { Value = uint.MinValue })); + + expression = ExpressionParser.ParsePredicate>("it => it.Value == uint.MinValue"); + func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = uint.MinValue })); + Assert.False(func(new ItemWithGenericProperty { Value = uint.MaxValue })); + } + + [Fact] + public void Should_SupportULongStaticMembers() + { + var expression = ExpressionParser.ParsePredicate>("it => it.Value == ulong.MaxValue"); + var func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = ulong.MaxValue })); + Assert.False(func(new ItemWithGenericProperty { Value = ulong.MinValue })); + + expression = ExpressionParser.ParsePredicate>("it => it.Value == ulong.MinValue"); + func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = ulong.MinValue })); + Assert.False(func(new ItemWithGenericProperty { Value = ulong.MaxValue })); + } + + [Fact] + public void Should_SupportUShortStaticMembers() + { + var expression = ExpressionParser.ParsePredicate>("it => it.Value == ushort.MaxValue"); + var func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = ushort.MaxValue })); + Assert.False(func(new ItemWithGenericProperty { Value = ushort.MinValue })); + + expression = ExpressionParser.ParsePredicate>("it => it.Value == ushort.MinValue"); + func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = ushort.MinValue })); + Assert.False(func(new ItemWithGenericProperty { Value = ushort.MaxValue })); + } + + [Fact] + public void Should_SupportBoolStaticMembers() + { + var expression = ExpressionParser.ParsePredicate>("it => it.Value == true"); + var func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = true })); + Assert.False(func(new ItemWithGenericProperty { Value = false })); + + expression = ExpressionParser.ParsePredicate>("it => it.Value == false"); + func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = false })); + Assert.False(func(new ItemWithGenericProperty { Value = true })); + } + + static Type? TypeLocator(string type) + { + return AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()).FirstOrDefault(t => t.FullName == type); + } + + [Fact] + public void Should_SupportEnumCasts() + { + var expression = ExpressionParser.ParsePredicate($"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_SupportNullableTypeCasts() + { + var expression = ExpressionParser.ParsePredicate>("it => it.Value == (int?)1"); + + var func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = 1 })); + } + + [Fact] + public void Should_SupportEnumerableExtensionMethods() + { + var expression = ExpressionParser.ParsePredicate>("it => it.Value.Contains(1)"); + + var func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = [1] })); + } + + [Fact] + public void Should_SupportNonGenericEnumerableExtensionMethods() + { + var expression = ExpressionParser.ParsePredicate>>("it => it.Value.Sum() == 1"); + + var func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty> { Value = new List { 1 } })); + } + + [Fact] + public void Should_SupportImplicitArrayInitialization() + { + var expression = ExpressionParser.ParsePredicate("it => (new []{\"Tofu\"}).Contains(it.Product.ProductName)"); + var func = expression.Compile(); + + Assert.True(func(new OrderDetail { Product = new Product { ProductName = "Tofu" } })); + } + + [Fact] + public void Should_SupportTypedArrayInitialization() + { + var expression = ExpressionParser.ParsePredicate("it => new bool?[]{ false }.Contains(it.Famous)"); + var func = expression.Compile(); + + Assert.True(func(new Person { Famous = false })); + } + + [Fact] + public void Should_SupportNullableProperties() + { + var expression = ExpressionParser.ParsePredicate("it => it.Age == 50"); + + var func = expression.Compile(); + + Assert.True(func(new Person { Age = 50 })); + } + + [Fact] + public void Should_SupportNullablePropertiesWithArray() + { + var expression = ExpressionParser.ParsePredicate("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("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>("it => it.Value == 50"); + + var func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = 50.0 })); + } + + [Fact] + public void Should_SupportToLower() + { + var expression = ExpressionParser.ParsePredicate("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_SupportArrayIndexAccess() + { + var expression = ExpressionParser.ParsePredicate>("it => it.Value[0] == 1"); + var func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = [1] })); + } + + [Fact] + public void Should_SupportDictionaryIndexAccess() + { + var expression = ExpressionParser.ParsePredicate>("it => (int)it[\"foo\"] == 1"); + var func = expression.Compile(); + Assert.True(func(new Dictionary { ["foo"] = 1 })); + } + + [Fact] + public void Should_SupportDictionaryIndexAccessWithNullableCast() + { + var expression = ExpressionParser.ParsePredicate>("it => (Int32?)it[\"foo\"] == null"); + var func = expression.Compile(); + Assert.True(func(new Dictionary { ["foo"] = null })); + } + + [Fact] + public void Should_SupportNullCoalescing() + { + var expression = ExpressionParser.ParsePredicate>("it => (it.Value ?? 0) == 0"); + + var func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = null })); + } + + [Fact] + public void Should_ParseChainedNullCoalescing() + { + var expression = ExpressionParser.ParseLambda("p => p.Name ?? p.Age?.ToString() ?? \"default\""); + + var func = expression.Compile(); + + Assert.Equal("default", func(new Person() { Name = null, Age = null })); + Assert.Equal("42", func(new Person() { Name = null, Age = 42 })); + Assert.Equal("John", func(new Person() { Name = "John", Age = 42 })); + } + + [Fact] + public void Should_ParseNestedNullCoalescing() + { + var expression = ExpressionParser.ParsePredicate("it => ((it.Name ?? it.Age?.ToString()) ?? \"default\").Contains(\"foo\")"); + + var func = expression.Compile(); + + Assert.True(func(new Person { Name = "foo" })); + Assert.False(func(new Person { Name = null, Age = null })); + } + + [Fact] + public void Should_SupportNullConditionAndCoalescence() + { + var expression = ExpressionParser.ParsePredicate("it => (((it == null) ? null : it.Name) ?? \"\").Contains(\"Da\")"); + + var func = expression.Compile(); + + Assert.True(func(new Person { Name = "Dali" })); + } + + [Fact] + public void Should_SupportNestedLambdas() + { + var expression = ExpressionParser.ParsePredicate("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_SupportUnaryNot() + { + var expression = ExpressionParser.ParsePredicate("it => !it.Famous"); + var func = expression.Compile(); + + Assert.True(func(new Person { Famous = false })); + Assert.False(func(new Person { Famous = true })); + } + + [Fact] + public void Should_SupportEnumWithoutCasts() + { + var typeLocator = (string type) => AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()).FirstOrDefault(t => t.FullName == type); + + var expression = ExpressionParser.ParsePredicate>("it => it.Value.Any(i => (new []{0}).Contains(i))", typeLocator); + + var func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = [CarType.Sedan] })); + } + + [Fact] + public void Should_SupportNestedLambdasWithEnums() + { + var expression = ExpressionParser.ParsePredicate("it => it.Statuses.Any(i => (new []{1}).Contains(i))"); + var func = expression.Compile(); + + Assert.True(func(new OrderDetail { Statuses = new List() { (Status)1 } })); + } + + [Fact] + public void Should_SupportNestedLambdasWithComplexMethod() + { + var expression = ExpressionParser.ParsePredicate("it => new [] { (Status)1 }.Intersect(it.Statuses).Any()", type => typeof(Status)); + var func = expression.Compile(); + + Assert.True(func(new OrderDetail { Statuses = new List() { (Status)1 } })); + } + + [Fact] + public void Should_CreateProjection() + { + var expression = ExpressionParser.ParseLambda("it => new { ProductName = it.Product.ProductName}"); + + var func = expression.Compile(); + + var result = func.DynamicInvoke(new OrderDetail { Product = new Product { ProductName = "Queso" } }); + + var property = result?.GetType().GetProperty("ProductName"); + + Assert.Equal(typeof(string), property?.PropertyType); + + Assert.Equal("Queso", property?.GetValue(result)); + } + + [Fact] + public void Should_CreateProjectionFromNestedAccess() + { + var expression = ExpressionParser.ParseLambda("it => new { it.Product.Category.CategoryName }"); + + var func = expression.Compile(); + + var orderDetail = new OrderDetail { Product = new Product { Category = new Category { CategoryName = "Beverages" } } }; + + var x = new { orderDetail?.Product?.Category?.CategoryName }; + + var result = func.DynamicInvoke(orderDetail); + + var property = result?.GetType().GetProperty("CategoryName"); + + Assert.Equal(typeof(string), property?.PropertyType); + + Assert.Equal("Beverages", property?.GetValue(result)); + } + + [Fact] + public void Should_CreateProjectionFromNestedConditionalAccess() + { + var expression = ExpressionParser.ParseLambda("it => new { it.Product?.Category?.CategoryName }"); + + var func = expression.Compile(); + + var orderDetail = new OrderDetail { Product = new Product { Category = new Category { CategoryName = "Beverages" } } }; + + var x = new { orderDetail?.Product?.Category?.CategoryName }; + + var result = func.DynamicInvoke(orderDetail); + + var property = result?.GetType().GetProperty("CategoryName"); + + Assert.Equal(typeof(string), property?.PropertyType); + + Assert.Equal("Beverages", property?.GetValue(result)); + } + + [Fact] + public void Should_CreateProjectionFromConditionalAccess() + { + var expression = ExpressionParser.ParseLambda("it => new { ProductName = it.Product?.ProductName}"); + var func = expression.Compile(); + var result = func.DynamicInvoke(new OrderDetail { Product = null }); + + var property = result?.GetType().GetProperty("ProductName"); + + Assert.Equal(typeof(string), property?.PropertyType); + Assert.Null(property?.GetValue(result)); + } + + [Fact] + public void Should_CreateProjectionFromNestedConditionalAccessAndAssignment() + { + var expression = ExpressionParser.ParseLambda("it => new { CategoryName = it.Product?.Category?.CategoryName}"); + var func = expression.Compile(); + var result = func.DynamicInvoke(new OrderDetail { Product = null }); + + var property = result?.GetType().GetProperty("CategoryName"); + + Assert.Equal(typeof(string), property?.PropertyType); + Assert.Null(property?.GetValue(result)); + } + + [Fact] + public void Should_SupportStringConcatAndSubstring() + { + var expression = ExpressionParser.ParsePredicate("it => (it.Name + \" Smith\").Substring(0, 5) == \"John \""); + var func = expression.Compile(); + + Assert.True(func(new Person { Name = "John" })); + Assert.False(func(new Person { Name = "Jane" })); + } + + [Fact] + public void Should_ParseScientificNotation() + { + // Test double with scientific notation + var doubleExpression = ExpressionParser.ParsePredicate>("it => it.Value == 1.23e4"); + var doubleFunc = doubleExpression.Compile(); + Assert.True(doubleFunc(new ItemWithGenericProperty { Value = 12300.0 })); + + // Test negative exponent + doubleExpression = ExpressionParser.ParsePredicate>("it => it.Value == 1.23e-4"); + doubleFunc = doubleExpression.Compile(); + Assert.True(doubleFunc(new ItemWithGenericProperty { Value = 0.000123 })); + + // Test with capital E + doubleExpression = ExpressionParser.ParsePredicate>("it => it.Value == 1.23E4"); + doubleFunc = doubleExpression.Compile(); + Assert.True(doubleFunc(new ItemWithGenericProperty { Value = 12300.0 })); + + // Test with float suffix + var floatExpression = ExpressionParser.ParsePredicate>("it => it.Value == 1.23e4f"); + var floatFunc = floatExpression.Compile(); + Assert.True(floatFunc(new ItemWithGenericProperty { Value = 12300.0f })); + } + + [Fact] + public void Should_ParseHexNumbers() + { + // Test int with hex number + var expression = ExpressionParser.ParsePredicate>("it => it.Value == 0x1A"); + var func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = 26 })); + } + +/* + [Fact] + public void Should_SupportComplexMathOperations() + { + var expression = ExpressionParser.ParsePredicate("it => Math.Pow(it.Age ?? 0, 2) + Math.Sqrt(16) == 34"); + var func = expression.Compile(); + Assert.True(func(new Person { Age = 5 })); + } +*/ + + [Fact] + public void Should_SupportDeepNestedPropertyAccess() + { + var f = (OrderDetail it) => it.Product?.Category?.CategoryName?.Length > 0; + + var expression = ExpressionParser.ParsePredicate("it => it.Product?.Category?.CategoryName?.Length > 0"); + var func = expression.Compile(); + Assert.True(func(new OrderDetail { Product = new Product { Category = new Category { CategoryName = "Test" } } })); + } + + [Fact] + public void Should_SupportDateTimeOperations() + { + var expression = ExpressionParser.ParsePredicate("it => (it.BirthDate.AddYears(1) - DateTime.Now).TotalDays > 0"); + var func = expression.Compile(); + Assert.True(func(new Person { BirthDate = DateTime.Now.AddDays(400) })); + } + + [Fact] + public void Should_SupportComplexConditionals() + { + var expression = ExpressionParser.ParsePredicate("it => (it.Age > 18 ? \"Adult\" : \"Minor\") == \"Adult\" && (it.Famous ?? false)"); + var func = expression.Compile(); + Assert.True(func(new Person { Age = 25, Famous = true })); + } + + [Fact] + public void Should_SupportMethodChaining() + { + var expression = ExpressionParser.ParsePredicate("it => it.Name?.Trim().ToUpper().Contains(\"JOHN\") == true"); + var func = expression.Compile(); + Assert.True(func(new Person { Name = " John " })); + } + + [Fact] + public void Should_SupportMultipleCasts() + { + var expression = ExpressionParser.ParsePredicate>("it => (int)(double)it.Value == 42"); + var func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = 42.0 })); + Assert.True(func(new ItemWithGenericProperty { Value = 42.5 })); // Will be truncated to 42 + Assert.False(func(new ItemWithGenericProperty { Value = 41.9 })); + } + + [Fact] + public void Should_SupportMultipleCastsWithNullable() + { + var expression = ExpressionParser.ParsePredicate>("it => (int?)(double?)it.Value == 42"); + var func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = 42.0 })); + Assert.True(func(new ItemWithGenericProperty { Value = 42.5 })); // Will be truncated to 42 + Assert.False(func(new ItemWithGenericProperty { Value = 41.9 })); + Assert.False(func(new ItemWithGenericProperty { Value = null })); // null is not equal to 42 + } + + [Fact] + public void Should_SupportMultipleCastsWithDifferentTypes() + { + var expression = ExpressionParser.ParsePredicate>("it => ((int)(double)it.Value).ToString() == \"42\""); + var func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = 42.0 })); + Assert.True(func(new ItemWithGenericProperty { Value = 42.5 })); // Will be truncated to 42 + Assert.False(func(new ItemWithGenericProperty { Value = 41.9 })); + } + + [Fact] + public void Should_SupportNestedTypeCasts() + { + var expression = ExpressionParser.ParsePredicate>("it => ((int)((double)((decimal)it.Value))).ToString() == \"42\""); + var func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = 42.0m })); + Assert.True(func(new ItemWithGenericProperty { Value = 42.5m })); // Will be truncated to 42 + Assert.False(func(new ItemWithGenericProperty { Value = 41.9m })); + } + + [Fact] + public void Should_SupportThreeLevelCasts() + { + var expression = ExpressionParser.ParsePredicate>("it => (int)(double)(decimal)it.Value == 42"); + var func = expression.Compile(); + + Assert.True(func(new ItemWithGenericProperty { Value = 42.0m })); + Assert.True(func(new ItemWithGenericProperty { Value = 42.5m })); // Will be truncated to 42 + Assert.False(func(new ItemWithGenericProperty { Value = 41.9m })); + } + + [Fact] + public void Should_SupportComplexArrayOperations() + { + var expression = ExpressionParser.ParsePredicate>("it => it.Value.Length > 2"); + var func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = new[] { 1, 2, 3 } })); + Assert.False(func(new ItemWithGenericProperty { Value = new[] { 1, 2 } })); + } + + [Fact] + public void Should_SupportNestedMethodCallsWithParameters() + { + var expression = ExpressionParser.ParsePredicate("p => p.Name.Substring(0, 4).ToUpper() == \"JOHN\""); + var func = expression.Compile(); + Assert.True(func(new Person { Name = "Johnny" })); + Assert.False(func(new Person { Name = "Jane" })); + } + + [Fact] + public void Should_SupportComplexStaticMethodCombinations() + { + var expression = ExpressionParser.ParsePredicate("p => Math.Abs(p.Age ?? 0) > 10"); + var func = expression.Compile(); + Assert.True(func(new Person { Age = 15 })); + Assert.False(func(new Person { Age = 5 })); + } + + + [Fact] + public void Should_SupportComplexTypeConversions() + { + var expression = ExpressionParser.ParsePredicate>("it => ((int)((double)((decimal)it.Value))).ToString() == \"42\""); + var func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = 42.0m })); + Assert.True(func(new ItemWithGenericProperty { Value = 42.5m })); // Will be truncated to 42 + Assert.False(func(new ItemWithGenericProperty { Value = 41.9m })); + } + + [Fact] + public void Should_SupportNestedTernaryOperations() + { + var expression = ExpressionParser.ParsePredicate("p => (p.Age > 18 ? \"Adult\" : \"Minor\") == \"Adult\""); + var func = expression.Compile(); + Assert.True(func(new Person { Age = 20 })); + Assert.False(func(new Person { Age = 15 })); + } + + [Fact] + public void Should_SupportComplexStringFormatting() + { + var expression = ExpressionParser.ParsePredicate("p => string.Format(\"{0}\", p.Name) == p.Name"); + var func = expression.Compile(); + Assert.True(func(new Person { Name = "John" })); + Assert.True(func(new Person { Name = "Jane" })); + } + + [Fact] + public void Should_SupportNestedCollectionOperations() + { + var expression = ExpressionParser.ParsePredicate("o => o.WorkStatuses.Count > 0"); + var func = expression.Compile(); + Assert.True(func(new OrderDetail { WorkStatuses = new List { new() { Name = "Completed" } } })); + Assert.False(func(new OrderDetail { WorkStatuses = new List() })); + } + + [Fact] + public void Should_SupportConvertMethods() + { + var expression = ExpressionParser.ParsePredicate>("it => Convert.ToInt32(it.Value) == 42"); + var func = expression.Compile(); + Assert.True(func(new ItemWithGenericProperty { Value = "42" })); + Assert.False(func(new ItemWithGenericProperty { Value = "41" })); + } + + [Fact] + public void Should_SelectByString() + { + var list = new List { 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_SelectByWithUntypedIQueryableString() + { + IQueryable list = new List { 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_RespectBinaryOperatorPrecedence() + { + // Test AND, OR, XOR precedence + var expression = ExpressionParser.ParseLambda, int>("p => p.Value & 3 | 4 ^ 1"); + var func = expression.Compile(); + Assert.Equal(25 & 3 | 4 ^ 1, func(new ItemWithGenericProperty { Value = 25 })); + + // Test shift operators precedence + expression = ExpressionParser.ParseLambda, int>("p => p.Value << 2 >> 1"); + func = expression.Compile(); + Assert.Equal(25 << 2 >> 1, func(new ItemWithGenericProperty { Value = 25 })); + + // Test arithmetic and shift precedence + expression = ExpressionParser.ParseLambda, int>("p => p.Value * 2 << 1 + 1"); + func = expression.Compile(); + Assert.Equal(25 * 2 << 1 + 1, func(new ItemWithGenericProperty { Value = 25 })); + + // Test complex combination of bitwise operators + expression = ExpressionParser.ParseLambda, int>("p => p.Value & 7 | 12 ^ 3 & 5"); + func = expression.Compile(); + Assert.Equal(25 & 7 | 12 ^ 3 & 5, func(new ItemWithGenericProperty { Value = 25 })); + + // Test arithmetic and bitwise operator precedence + expression = ExpressionParser.ParseLambda, int>("p => p.Value * 2 & 15 + 3"); + func = expression.Compile(); + Assert.Equal(25 * 2 & 15 + 3, func(new ItemWithGenericProperty { Value = 25 })); + + // Test shift and bitwise operator precedence + expression = ExpressionParser.ParseLambda, int>("p => p.Value << 2 & 15 | 7"); + func = expression.Compile(); + Assert.Equal(25 << 2 & 15 | 7, func(new ItemWithGenericProperty { Value = 25 })); + + // Test parentheses with mixed operators + expression = ExpressionParser.ParseLambda, int>("p => (p.Value & 15) << (2 + 1)"); + func = expression.Compile(); + Assert.Equal((25 & 15) << (2 + 1), func(new ItemWithGenericProperty { Value = 25 })); + + // Test complex expression with multiple operators and parentheses + expression = ExpressionParser.ParseLambda, int>("p => (p.Value * 2 + 1) << 2 & 15 | 7 ^ 3"); + func = expression.Compile(); + Assert.Equal((25 * 2 + 1) << 2 & 15 | 7 ^ 3, func(new ItemWithGenericProperty { Value = 25 })); + + // Test shift operators with multiplication and addition + expression = ExpressionParser.ParseLambda, int>("p => p.Value * 2 >> 1 + 1 << 2"); + func = expression.Compile(); + Assert.Equal(25 * 2 >> 1 + 1 << 2, func(new ItemWithGenericProperty { Value = 25 })); + + // Test nested parentheses with mixed operators + expression = ExpressionParser.ParseLambda, int>("p => ((p.Value & 7) << 2) | (15 & (3 + 2))"); + func = expression.Compile(); + Assert.Equal(((25 & 7) << 2) | (15 & (3 + 2)), func(new ItemWithGenericProperty { Value = 25 })); + } +} \ No newline at end of file diff --git a/Radzen.Blazor.Tests/ExpressionSerializerTests.cs b/Radzen.Blazor.Tests/ExpressionSerializerTests.cs new file mode 100644 index 000000000..3b7f715c5 --- /dev/null +++ b/Radzen.Blazor.Tests/ExpressionSerializerTests.cs @@ -0,0 +1,304 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Xunit; + +namespace Radzen.Blazor.Tests +{ + class TestEntity + { + public string Name { get; set; } + public int Age { get; set; } + public double Salary { get; set; } + public float Score { get; set; } + public decimal Balance { get; set; } + public short Level { get; set; } + public long Population { get; set; } + public Status AccountStatus { get; set; } + public DateTime CreatedAt { get; set; } + public DateTimeOffset LastUpdated { get; set; } + public Guid Id { get; set; } + public TimeOnly StartTime { get; set; } + public DateOnly BirthDate { get; set; } + public int[] Scores { get; set; } + public List Tags { get; set; } + public List Children { get; set; } + public Address Address { get; set; } + public double[] Salaries { get; set; } + public float[] Heights { get; set; } + public decimal[] Balances { get; set; } + public short[] Levels { get; set; } + public long[] Populations { get; set; } + public string[] Names { get; set; } + public Guid[] Ids { get; set; } + public DateTime[] CreatedDates { get; set; } + public DateTimeOffset[] UpdatedDates { get; set; } + public TimeOnly[] StartTimes { get; set; } + public DateOnly[] BirthDates { get; set; } + public Status[] Statuses { get; set; } + } + + enum Status + { + Active, + Inactive, + Suspended + } + + class Address + { + public string City { get; set; } + public string Country { get; set; } + } + + public class ExpressionSerializerTests + { + private readonly ExpressionSerializer _serializer = new ExpressionSerializer(); + + [Fact] + public void Serializes_SimpleBinaryExpression() + { + Expression> expr = e => e > 10; + Assert.Equal("e => (e > 10)", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_StringEquality() + { + Expression> expr = e => e.Name == "John"; + Assert.Equal("e => (e.Name == \"John\")", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_IntComparison() + { + Expression> expr = e => e.Age > 18; + Assert.Equal("e => (e.Age > 18)", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_DoubleComparison() + { + Expression> expr = e => e.Salary < 50000.50; + Assert.Equal("e => (e.Salary < 50000.5)", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_FloatComparison() + { + Expression> expr = e => e.Score >= 85.3f; + Assert.Equal("e => (e.Score >= 85.3)", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_DecimalComparison() + { + Expression> expr = e => e.Balance <= 1000.75m; + Assert.Equal("e => (e.Balance <= 1000.75)", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_ShortComparison() + { + Expression> expr = e => e.Level == 3; + Assert.Equal("e => (e.Level == 3)", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_LongComparison() + { + Expression> expr = e => e.Population > 1000000L; + Assert.Equal("e => (e.Population > 1000000)", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_EnumComparison() + { + Expression> expr = e => e.AccountStatus == Status.Inactive; + Assert.Equal("e => (e.AccountStatus == 1)", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_ArrayContainsValue() + { + Expression> expr = e => e.Scores.Contains(100); + Assert.Equal("e => e.Scores.Contains(100)", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_ArrayNotContainsValue() + { + Expression> expr = e => !e.Scores.Contains(100); + Assert.Equal("e => (!e.Scores.Contains(100))", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_ArrayInValue() + { + Expression> expr = e => e.Scores.Intersect(new [] { 100 }).Any(); + Assert.Equal("e => e.Scores.Intersect(new [] { 100 }).Any()", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_ArrayNotInValue() + { + Expression> expr = e => e.Scores.Except(new[] { 100 }).Any(); + Assert.Equal("e => e.Scores.Except(new [] { 100 }).Any()", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_ArrayInValueOposite() + { + Expression> expr = e => new[] { 100 }.Intersect(e.Scores).Any(); + Assert.Equal("e => new [] { 100 }.Intersect(e.Scores).Any()", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_ArrayNotInValueOposite() + { + Expression> expr = e => new[] { 100 }.Except(e.Scores).Any(); + Assert.Equal("e => new [] { 100 }.Except(e.Scores).Any()", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_IntArrayInValueOposite() + { + Expression> expr = e => new[] { 100 }.Intersect(e.Scores).Any(); + Assert.Equal("e => new [] { 100 }.Intersect(e.Scores).Any()", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_IntArrayNotInValueOposite() + { + Expression> expr = e => !new[] { 100 }.Intersect(e.Scores).Any(); + Assert.Equal("e => (!new [] { 100 }.Intersect(e.Scores).Any())", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_DoubleArrayInValueOposite() + { + Expression> expr = e => new[] { 99.99 }.Intersect(e.Salaries).Any(); + Assert.Equal("e => new [] { 99.99 }.Intersect(e.Salaries).Any()", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_FloatArrayInValueOposite() + { + Expression> expr = e => new[] { 5.5f }.Intersect(e.Heights).Any(); + Assert.Equal("e => new [] { 5.5 }.Intersect(e.Heights).Any()", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_DecimalArrayInValueOposite() + { + Expression> expr = e => new[] { 1000.75m }.Intersect(e.Balances).Any(); + Assert.Equal("e => new [] { 1000.75 }.Intersect(e.Balances).Any()", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_ShortArrayInValueOposite() + { + Expression> expr = e => new [] { (short)3 }.Intersect(e.Levels).Any(); + Assert.Equal("e => new [] { 3 }.Intersect(e.Levels).Any()", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_LongArrayInValueOposite() + { + Expression> expr = e => new [] { 1000000L }.Intersect(e.Populations).Any(); + Assert.Equal("e => new [] { 1000000 }.Intersect(e.Populations).Any()", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_StringArrayInValueOposite() + { + Expression> expr = e => new[] { "Alice", "Bob" }.Intersect(e.Names).Any(); + Assert.Equal("e => (new [] { \"Alice\", \"Bob\" }).Intersect(e.Names).Any()", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_GuidArrayInValueOposite() + { + Expression> expr = e => new[] { Guid.Parse("12345678-1234-1234-1234-123456789abc") }.Intersect(e.Ids).Any(); + Assert.Equal("e => (new [] { Guid.Parse(\"12345678-1234-1234-1234-123456789abc\") }).Intersect(e.Ids).Any()", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_DateTimeArrayInValueOposite() + { + Expression> expr = e => new[] { DateTime.Parse("2023-01-01T00:00:00.000Z") }.Intersect(e.CreatedDates).Any(); + Assert.Equal("e => (new [] { DateTime.Parse(\"2023-01-01T00:00:00.000Z\") }).Intersect(e.CreatedDates).Any()", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_DateTimeOffsetArrayInValueOposite() + { + Expression> expr = e => new[] { DateTimeOffset.Parse("2023-01-01T10:30:00.000+00:00") }.Intersect(e.UpdatedDates).Any(); + Assert.Equal("e => (new [] { DateTimeOffset.Parse(\"2023-01-01T10:30:00.000+00:00\") }).Intersect(e.UpdatedDates).Any()", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_TimeOnlyArrayInValueOposite() + { + Expression> expr = e => new[] { TimeOnly.Parse("12:00:00") }.Intersect(e.StartTimes).Any(); + Assert.Equal("e => (new [] { TimeOnly.Parse(\"12:00:00\") }).Intersect(e.StartTimes).Any()", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_DateOnlyArrayInValueOposite() + { + Expression> expr = e => new[] { DateOnly.Parse("2000-01-01") }.Intersect(e.BirthDates).Any(); + Assert.Equal("e => (new [] { DateOnly.Parse(\"2000-01-01\") }).Intersect(e.BirthDates).Any()", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_EnumArrayInValueOposite() + { + Expression> expr = e => new[] { Status.Active, Status.Inactive }.Intersect(e.Statuses).Any(); + Assert.Equal("e => (new [] { (Radzen.Blazor.Tests.Status)0, (Radzen.Blazor.Tests.Status)1 }).Intersect(e.Statuses).Any()", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_ListContainsValue() + { + Expression> expr = e => e.Tags.Contains("VIP"); + Assert.Equal("e => e.Tags.Contains(\"VIP\")", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_ListNotContainsValue() + { + Expression> expr = e => !e.Tags.Contains("VIP"); + Assert.Equal("e => (!e.Tags.Contains(\"VIP\"))", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_ListAnyCheck() + { + Expression> expr = e => e.Children.Any(c => c.Age > 18); + Assert.Equal("e => e.Children.Any(c => (c.Age > 18))", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_ListNotAnyCheck() + { + Expression> expr = e => !e.Children.Any(c => c.Age > 18); + Assert.Equal("e => (!e.Children.Any(c => (c.Age > 18)))", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_EntitySubPropertyCheck() + { + Expression> expr = e => e.Address.City == "New York"; + Assert.Equal("e => (e.Address.City == \"New York\")", _serializer.Serialize(expr)); + } + + [Fact] + public void Serializes_ComplexExpressionWithProperties() + { + Expression> expr = e => e.Age > 18 && e.Tags.Contains("Member") || e.Address.City == "London"; + Assert.Equal("e => (((e.Age > 18) && e.Tags.Contains(\"Member\")) || (e.Address.City == \"London\"))", _serializer.Serialize(expr)); + } + } +} \ No newline at end of file diff --git a/Radzen.Blazor.Tests/FieldsetTests.cs b/Radzen.Blazor.Tests/FieldsetTests.cs index f39d1d55d..39c64f86c 100644 --- a/Radzen.Blazor.Tests/FieldsetTests.cs +++ b/Radzen.Blazor.Tests/FieldsetTests.cs @@ -184,13 +184,13 @@ namespace Radzen.Blazor.Tests Assert.Contains("SummaryContent", component.Markup); Assert.Equal( - "", - component.Find(".rz-fieldset-content-summary").ParentElement.Attributes.First(attr => attr.Name == "style").Value + "false", + component.Find(".rz-fieldset-content-summary").ParentElement.ParentElement.Attributes.First(attr => attr.Name == "aria-hidden").Value ); } [Fact] - public void Fieldset_DontRenders_SummaryWhenOpen() + public void Fieldset_DoesNotRender_SummaryWhenOpen() { using var ctx = new TestContext(); var component = ctx.RenderComponent(); @@ -210,8 +210,8 @@ namespace Radzen.Blazor.Tests Assert.Contains("SummaryContent", component.Markup); Assert.Equal( - "display: none", - component.Find(".rz-fieldset-content-summary").ParentElement.Attributes.First(attr => attr.Name == "style").Value + "true", + component.Find(".rz-fieldset-content-summary").ParentElement.ParentElement.Attributes.First(attr => attr.Name == "aria-hidden").Value ); } } diff --git a/Radzen.Blazor.Tests/PanelTests.cs b/Radzen.Blazor.Tests/PanelTests.cs index cbe3869cd..050f0fc88 100644 --- a/Radzen.Blazor.Tests/PanelTests.cs +++ b/Radzen.Blazor.Tests/PanelTests.cs @@ -198,14 +198,10 @@ namespace Radzen.Blazor.Tests }); Assert.Contains("SummaryContent", component.Markup); - Assert.Equal( - "display: block", - component.Find(".rz-panel-content-summary").ParentElement.Attributes.First(attr => attr.Name == "style").Value - ); } [Fact] - public void Panel_DontRenders_SummaryWhenOpen() + public void Panel_DoesNotRender_SummaryWhenOpen() { using var ctx = new TestContext(); var component = ctx.RenderComponent(); @@ -225,8 +221,8 @@ namespace Radzen.Blazor.Tests Assert.Contains("SummaryContent", component.Markup); Assert.Equal( - "display: none", - component.Find(".rz-panel-content-summary").ParentElement.Attributes.First(attr => attr.Name == "style").Value + "true", + component.Find(".rz-panel-content-summary").ParentElement.ParentElement.Attributes.First(attr => attr.Name == "aria-hidden").Value ); } } diff --git a/Radzen.Blazor/DynamicTypeFactory.cs b/Radzen.Blazor/DynamicTypeFactory.cs new file mode 100644 index 000000000..7c3c8a780 --- /dev/null +++ b/Radzen.Blazor/DynamicTypeFactory.cs @@ -0,0 +1,56 @@ +using System; +using System.Reflection; +using System.Reflection.Emit; + +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, + [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; + } +} \ No newline at end of file diff --git a/Radzen.Blazor/ExpressionLexer.cs b/Radzen.Blazor/ExpressionLexer.cs new file mode 100644 index 000000000..a5ed45b28 --- /dev/null +++ b/Radzen.Blazor/ExpressionLexer.cs @@ -0,0 +1,873 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq.Expressions; +using System.Text; + +namespace Radzen; + +class Token +{ + public TokenType Type { get; set; } + public string Value { get; set; } = string.Empty; + public ValueKind ValueKind { get; set; } = ValueKind.None; + public int IntValue { get; internal set; } + public uint UintValue { get; internal set; } + public long LongValue { get; internal set; } + public ulong UlongValue { get; internal set; } + public decimal DecimalValue { get; internal set; } + public float FloatValue { get; internal set; } + public double DoubleValue { get; internal set; } + + public Token(TokenType type) + { + Type = type; + } + + public Token(TokenType type, string value) + { + Type = type; + Value = value; + } + + public ConstantExpression ToConstantExpression() + { + return ValueKind switch + { + ValueKind.Null => Expression.Constant(null), + ValueKind.String => Expression.Constant(Value), + ValueKind.Character => Expression.Constant(Value[0]), + ValueKind.Int => Expression.Constant(IntValue), + ValueKind.UInt => Expression.Constant(UintValue), + ValueKind.Long => Expression.Constant(LongValue), + ValueKind.ULong => Expression.Constant(UlongValue), + ValueKind.Float => Expression.Constant(FloatValue), + ValueKind.Double => Expression.Constant(DoubleValue), + ValueKind.Decimal => Expression.Constant(DecimalValue), + ValueKind.True => Expression.Constant(true), + ValueKind.False => Expression.Constant(false), + _ => throw new InvalidOperationException($"Unsupported value kind: {ValueKind}") + }; + } +} + +enum ValueKind +{ + None, + String, + Int, + Float, + Double, + Decimal, + Character, + Null, + True, + False, + Long, + UInt, + ULong, +} + + +enum TokenType +{ + None, + Identifier, + EqualsEquals, + NotEquals, + EqualsGreaterThan, + StringLiteral, + NumericLiteral, + Dot, + OpenParen, + CloseParen, + Comma, + AmpersandAmpersand, + Ampersand, + BarBar, + Bar, + GreaterThan, + LessThan, + LessThanOrEqual, + GreaterThanOrEqual, + Plus, + Minus, + Star, + Slash, + CharacterLiteral, + QuestionMark, + QuestionMarkQuestionMark, + Colon, + QuestionDot, + New, + NullLiteral, + TrueLiteral, + FalseLiteral, + OpenBracket, + CloseBracket, + OpenBrace, + CloseBrace, + ExclamationMark, + Equals, + Caret, + GreaterThanGreaterThan, + LessThanLessThan, +} + +static class TokenTypeExtensions +{ + public static ExpressionType ToExpressionType(this TokenType tokenType) + { + return tokenType switch + { + TokenType.EqualsEquals => ExpressionType.Equal, + TokenType.NotEquals => ExpressionType.NotEqual, + TokenType.EqualsGreaterThan => ExpressionType.GreaterThanOrEqual, + TokenType.AmpersandAmpersand => ExpressionType.AndAlso, + TokenType.Ampersand => ExpressionType.And, + TokenType.BarBar => ExpressionType.OrElse, + TokenType.Bar => ExpressionType.Or, + TokenType.GreaterThan => ExpressionType.GreaterThan, + TokenType.LessThan => ExpressionType.LessThan, + TokenType.LessThanOrEqual => ExpressionType.LessThanOrEqual, + TokenType.GreaterThanOrEqual => ExpressionType.GreaterThanOrEqual, + TokenType.Plus => ExpressionType.Add, + TokenType.Minus => ExpressionType.Subtract, + TokenType.Star => ExpressionType.Multiply, + TokenType.Slash => ExpressionType.Divide, + TokenType.Caret => ExpressionType.ExclusiveOr, + TokenType.GreaterThanGreaterThan => ExpressionType.RightShift, + TokenType.LessThanLessThan => ExpressionType.LeftShift, + _ => throw new InvalidOperationException($"Unsupported token type: {tokenType}") + }; + } +} + +class ExpressionLexer(string expression) +{ + private int position; + + private char Peek(int offset = 0) + { + if (position + offset >= expression.Length) + { + return '\0'; + } + + return expression[position + offset]; + } + + private void Advance(int count) + { + position += count; + } + + bool TryAdvance(char expected) + { + if (Peek(1) == expected) + { + Advance(1); + return true; + } + + return false; + } + + private void ScanTrivia() + { + while (char.IsWhiteSpace(Peek())) + { + Advance(1); + } + } + + public static List Scan(string expression) + { + var lexer = new ExpressionLexer(expression); + + return [.. lexer.Scan()]; + } + + public IEnumerable Scan() + { + while (position < expression.Length) + { + ScanTrivia(); + + var token = ScanToken(); + + if (token.Type == TokenType.None) + { + yield break; + } + + yield return token; + } + + yield return new Token(TokenType.None, string.Empty); + } + + private Token ScanToken() + { + var ch = Peek(); + + switch (ch) + { + case '"': + return ScanStringLiteral(); + case '\'': + return ScanCharacterLiteral(); + case '=': + if (TryAdvance('=')) + { + Advance(1); + return new Token(TokenType.EqualsEquals); + } + + if (TryAdvance('>')) + { + Advance(1); + return new Token(TokenType.EqualsGreaterThan); + } + + Advance(1); + return new Token(TokenType.Equals); + case '!': + if (TryAdvance('=')) + { + Advance(1); + return new Token(TokenType.NotEquals); + } + Advance(1); + return new Token(TokenType.ExclamationMark); + case '>': + if (TryAdvance('=')) + { + Advance(1); + return new Token(TokenType.GreaterThanOrEqual); + } + if (TryAdvance('>')) + { + Advance(1); + return new Token(TokenType.GreaterThanGreaterThan); + } + Advance(1); + return new Token(TokenType.GreaterThan); + case '<': + if (TryAdvance('<')) + { + Advance(1); + return new Token(TokenType.LessThanLessThan); + } + if (TryAdvance('=')) + { + Advance(1); + return new Token(TokenType.LessThanOrEqual); + } + Advance(1); + return new Token(TokenType.LessThan); + case '+': + Advance(1); + return new Token(TokenType.Plus); + case '-': + Advance(1); + return new Token(TokenType.Minus); + case '*': + Advance(1); + return new Token(TokenType.Star); + case '/': + Advance(1); + return new Token(TokenType.Slash); + case '.': + Advance(1); + return new Token(TokenType.Dot); + case '(': + Advance(1); + return new Token(TokenType.OpenParen); + case ')': + Advance(1); + return new Token(TokenType.CloseParen); + case '[': + Advance(1); + return new Token(TokenType.OpenBracket); + case ']': + Advance(1); + return new Token(TokenType.CloseBracket); + case '{': + Advance(1); + return new Token(TokenType.OpenBrace); + case '}': + Advance(1); + return new Token(TokenType.CloseBrace); + case ',': + Advance(1); + return new Token(TokenType.Comma); + case '&': + if (TryAdvance('&')) + { + Advance(1); + return new Token(TokenType.AmpersandAmpersand); + } + Advance(1); + return new Token(TokenType.Ampersand); + case '|': + if (TryAdvance('|')) + { + Advance(1); + return new Token(TokenType.BarBar); + } + Advance(1); + return new Token(TokenType.Bar); + case '?': + if (TryAdvance('.')) + { + Advance(1); + return new Token(TokenType.QuestionDot); + } + + if (TryAdvance('?')) + { + Advance(1); + return new Token(TokenType.QuestionMarkQuestionMark); + } + + Advance(1); + return new Token(TokenType.QuestionMark); + case ':': + Advance(1); + return new Token(TokenType.Colon); + case '^': + Advance(1); + return new Token(TokenType.Caret); + case >= '0' and <= '9': + return ScanNumericLiteral(); + case '_': + case (>= 'a' and <= 'z') or (>= 'A' and <= 'Z'): + var token = ScanIdentifier(); + + return token.Value switch + { + "null" => new Token(TokenType.NullLiteral) { ValueKind = ValueKind.Null }, + "true" => new Token(TokenType.TrueLiteral) { ValueKind = ValueKind.True }, + "false" => new Token(TokenType.FalseLiteral) { ValueKind = ValueKind.False }, + "new" => new Token(TokenType.New), + _ => token + }; + } + + return new Token(TokenType.None, string.Empty); + } + + private char ScanEscapeSequence() + { + var ch = Peek(); + + Advance(1); + + switch (ch) + { + case '\'': + case '"': + case '\\': + break; + case '0': + ch = '\u0000'; + break; + case 'a': + ch = '\u0007'; + break; + case 'b': + ch = '\u0008'; + break; + case 'f': + ch = '\u000c'; + break; + case 'n': + ch = '\u000a'; + break; + case 'r': + ch = '\u000d'; + break; + case 't': + ch = '\u0009'; + break; + case 'v': + ch = '\u000b'; + break; + case 'u': + case 'U': + case 'x': + ch = ScanUnicodeEscapeSequence(ch); + break; + default: + throw new InvalidOperationException($"Invalid escape sequence '\\{ch}' at position {position}."); + } + + return ch; + } + + private char ScanUnicodeEscapeSequence(char ch) + { + var value = 0; + + var count = ch == 'U' ? 8 : 4; + + for (var i = 0; i < count; i++) + { + var digit = Peek(); + + int digitValue; + + if (digit >= '0' && digit <= '9') + { + digitValue = digit - '0'; + } + else if (digit >= 'a' && digit <= 'f') + { + digitValue = digit - 'a' + 10; + } + else if (digit >= 'A' && digit <= 'F') + { + digitValue = digit - 'A' + 10; + } + else if (ch != 'x') + { + throw new InvalidOperationException($"Invalid unicode escape sequence at position {position}."); + } + else + { + break; + } + + value = (value << 4) + digitValue; + + Advance(1); + } + + return (char)value; + } + + private Token ScanCharacterLiteral() + { + Advance(1); + + var buffer = new StringBuilder(); + + while (true) + { + var ch = Peek(); + + switch (ch) + { + case '\0': + throw new InvalidOperationException($"Unexpected end of character literal at position {position}."); + case '\\': + Advance(1); + buffer.Append(ScanEscapeSequence()); + break; + case '\'': + Advance(1); + + return new Token(TokenType.CharacterLiteral, buffer.ToString()) + { + ValueKind = ValueKind.Character + }; + default: + if (buffer.Length > 0) + { + throw new InvalidOperationException($"Too many characters in character literal at position {position}."); + } + + buffer.Append(ch); + Advance(1); + break; + } + } + } + + private Token ScanStringLiteral() + { + Advance(1); + + var buffer = new StringBuilder(); + + while (true) + { + var ch = Peek(); + + switch (ch) + { + case '\0': + throw new InvalidOperationException($"Unexpected end of string literal at position {position}."); + case '\\': + Advance(1); + buffer.Append(ScanEscapeSequence()); + break; + case '"': + Advance(1); + return new Token(TokenType.StringLiteral, buffer.ToString()) + { + ValueKind = ValueKind.String + }; + default: + buffer.Append(ch); + Advance(1); + break; + } + } + } + + private Token ScanNumericLiteral() + { + var buffer = new StringBuilder(); + var hasDecimal = false; + var hasFSuffix = false; + var hasDSuffix = false; + var hasMSuffix = false; + var hasLSuffix = false; + var hasExponent = false; + var hasHex = false; + var hasUSuffix = false; + + while (true) + { + var ch = Peek(); + + if (ch == '0') + { + var next = Peek(1); + + if (next == 'x' || next == 'X') + { + hasHex = true; + Advance(2); + continue; + } + } + + if (ch >= '0' && ch <= '9') + { + buffer.Append(ch); + Advance(1); + continue; + } + + if (ch == '.') + { + if (hasDecimal) + { + throw new InvalidOperationException($"Unexpected character '{ch}' at position {position}."); + } + + hasDecimal = true; + + buffer.Append(ch); + + Advance(1); + + continue; + } + + if (ch == 'l' || ch == 'L') + { + if (hasLSuffix) + { + throw new InvalidOperationException($"Unexpected character '{ch}' at position {position}."); + } + + hasLSuffix = true; + + Advance(1); + + continue; + } + + if (ch == 'u' || ch == 'U') + { + if (hasUSuffix) + { + throw new InvalidOperationException($"Unexpected character '{ch}' at position {position}."); + } + + hasUSuffix = true; + + Advance(1); + + continue; + } + + if (hasHex && ((ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F'))) + { + buffer.Append(ch); + Advance(1); + continue; + } + + if (ch == 'e' || ch == 'E') + { + if (hasExponent) + { + throw new InvalidOperationException($"Unexpected character '{ch}' at position {position}."); + } + + hasExponent = true; + buffer.Append(ch); + Advance(1); + + // Check for optional + or - after e/E + ch = Peek(); + if (ch == '+' || ch == '-') + { + buffer.Append(ch); + Advance(1); + } + + // Must have at least one digit after e/E + ch = Peek(); + + if (ch < '0' || ch > '9') + { + throw new InvalidOperationException($"Expected digit after exponent at position {position}."); + } + + continue; + } + + if (hasDecimal || hasExponent) + { + switch (ch) + { + case 'F': + case 'f': + hasFSuffix = true; + Advance(1); + break; + case 'D': + case 'd': + hasDSuffix = true; + Advance(1); + break; + case 'M': + case 'm': + hasMSuffix = true; + Advance(1); + break; + } + } + + break; + } + + var value = new Token(TokenType.NumericLiteral); + + var valueKind = ValueKind.None; + + if (hasDecimal || hasExponent) + { + valueKind = ValueKind.Double; + } + + if (hasFSuffix) + { + valueKind = ValueKind.Float; + } + + if (hasDSuffix) + { + valueKind = ValueKind.Double; + } + + if (hasMSuffix) + { + valueKind = ValueKind.Decimal; + } + + switch (valueKind) + { + case ValueKind.Float: + value.ValueKind = ValueKind.Float; + value.FloatValue = GetValueFloat(buffer.ToString()); + break; + case ValueKind.Double: + value.ValueKind = ValueKind.Double; + value.DoubleValue = GetValueDouble(buffer.ToString()); + break; + case ValueKind.Decimal: + value.ValueKind = ValueKind.Decimal; + value.DecimalValue = GetValueDecimal(buffer.ToString()); + break; + default: + var val = GetValueUInt64(buffer.ToString(), hasHex); + + if (!hasUSuffix && !hasLSuffix) + { + if (val <= Int32.MaxValue) + { + value.ValueKind = ValueKind.Int; + value.IntValue = (int)val; + } + else if (val <= UInt32.MaxValue) + { + value.ValueKind = ValueKind.UInt; + value.UintValue = (uint)val; + } + else if (val <= Int64.MaxValue) + { + value.ValueKind = ValueKind.Long; + value.LongValue = (long)val; + } + else + { + value.ValueKind = ValueKind.ULong; + value.UlongValue = val; + } + } + else if (hasUSuffix && !hasLSuffix) + { + if (val <= UInt32.MaxValue) + { + value.ValueKind = ValueKind.UInt; + value.UintValue = (uint)val; + } + else + { + value.ValueKind = ValueKind.ULong; + value.UlongValue = val; + } + } + else if (!hasUSuffix & hasLSuffix) + { + if (val <= Int64.MaxValue) + { + value.ValueKind = ValueKind.Long; + value.LongValue = (long)val; + } + else + { + value.ValueKind = ValueKind.ULong; + value.UlongValue = val; + } + } + else + { + value.ValueKind = ValueKind.ULong; + value.UlongValue = val; + } + break; + } + + return value; + } + + private static decimal GetValueDecimal(string text) + { + if (!decimal.TryParse(text, NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out var result)) + { + throw new InvalidOperationException($"Invalid numeric literal: {text}"); + } + + return result; + } + + private static float GetValueFloat(string text) + { + if (!float.TryParse(text, NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out var result)) + { + throw new InvalidOperationException($"Invalid numeric literal: {text}"); + } + + return result; + } + + private static double GetValueDouble(string text) + { + if (!double.TryParse(text, NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out var result)) + { + throw new InvalidOperationException($"Invalid numeric literal: {text}"); + } + + return result; + } + + private static ulong GetValueUInt64(string text, bool isHex) + { + if (!UInt64.TryParse(text, isHex ? NumberStyles.AllowHexSpecifier : NumberStyles.None, CultureInfo.InvariantCulture, out var result)) + { + throw new InvalidOperationException($"Invalid numeric literal: {text}"); + } + + return result; + } + + private Token ScanIdentifier() + { + var startOffset = position; + + while (true) + { + if (position == expression.Length) + { + var length = position - startOffset; + + return new Token(TokenType.Identifier, expression.Substring(startOffset, length)); + } + + switch (Peek()) + { + case '\0': + case ' ': + case '\r': + case '\n': + case '\t': + case '!': + case '%': + case '(': + case ')': + case '*': + case '+': + case ',': + case '-': + case '.': + case '/': + case ':': + case ';': + case '<': + case '=': + case '>': + case '?': + case '[': + case ']': + case '^': + case '{': + case '|': + case '}': + case '~': + case '"': + case '\'': + // All of the following characters are not valid in an + // identifier. If we see any of them, then we know we're + // done. + return new Token(TokenType.Identifier, expression[startOffset..position]); + case >= '0' and <= '9': + if (position == startOffset) + { + break; + } + else + { + goto case '_'; + } + case (>= 'a' and <= 'z') or (>= 'A' and <= 'Z'): + case '_': + // All of these characters are valid inside an identifier. + // consume it and keep processing. + Advance(1); + continue; + + default: + throw new InvalidOperationException($"Unexpected character '{Peek()}' at position {position}."); + } + } + } +} \ No newline at end of file diff --git a/Radzen.Blazor/ExpressionParser.cs b/Radzen.Blazor/ExpressionParser.cs index 48214d812..86c80da88 100644 --- a/Radzen.Blazor/ExpressionParser.cs +++ b/Radzen.Blazor/ExpressionParser.cs @@ -1,264 +1,903 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; using System; -using System.Linq; +using System.Collections.Generic; +using System.Globalization; using System.Linq.Expressions; using System.Reflection; -using System.Reflection.Emit; +using System.Text; +using System.Linq; namespace Radzen; -static class DynamicTypeFactory +#nullable enable + +/// +/// Parse lambda expressions from strings. +/// +public class ExpressionParser { - public static Type CreateType(string typeName, string[] propertyNames, Type[] propertyTypes) + /// + /// Parses a lambda expression that returns a boolean value. + /// + public static Expression> ParsePredicate(string expression, Func? typeResolver = null) { - if (propertyNames.Length != propertyTypes.Length) + return ParseLambda(expression, typeResolver); + } + + /// + /// Parses a lambda expression that returns a typed result. + /// + public static Expression> ParseLambda(string expression, Func? typeResolver = null) + { + var lambda = ParseLambda(expression, typeof(T), typeResolver); + + return Expression.Lambda>(lambda.Body, lambda.Parameters[0]); + } + + /// + /// Parses a lambda expression that returns untyped result. + /// + public static LambdaExpression ParseLambda(string expression, Func? typeLocator = null) + { + return ParseLambda(expression, typeof(T), typeLocator); + } + + /// + /// Parses a lambda expression that returns untyped result. + /// + public static LambdaExpression ParseLambda(string expression, Type type, Func? typeResolver = null) + { + var parser = new ExpressionParser(expression, typeResolver); + + return parser.ParseLambda(type); + } + + private readonly List tokens; + private int position = 0; + private readonly Func? typeResolver; + private readonly Stack parameterStack = new(); + + private ExpressionParser(string expression, Func? typeResolver = null) + { + this.typeResolver = typeResolver; + tokens = ExpressionLexer.Scan(expression); + } + + Token Expect(TokenType type) + { + if (position >= tokens.Count) { - throw new ArgumentException("Property names and types count mismatch."); + throw new InvalidOperationException($"Unexpected end of expression. Expected token: {type}"); } - var assemblyName = new AssemblyName("DynamicTypesAssembly"); - var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); - var moduleBuilder = assemblyBuilder.DefineDynamicModule("DynamicTypesModule"); + var token = tokens[position]; - var typeBuilder = moduleBuilder.DefineType(typeName, TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.Sealed); - - for (int i = 0; i < propertyNames.Length; i++) + if (token.Type != type) { - 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, - [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); + throw new InvalidOperationException($"Unexpected token: {token.Type}. Expected: {type}"); } - var dynamicType = typeBuilder.CreateType(); - return dynamicType; - } -} + position++; -class ExpressionSyntaxVisitor : CSharpSyntaxVisitor -{ - private readonly ParameterExpression parameter; - private readonly Func typeLocator; - - public ExpressionSyntaxVisitor(ParameterExpression parameter, Func typeLocator) - { - this.parameter = parameter; - this.typeLocator = typeLocator; + return token; } - public override Expression VisitBinaryExpression(BinaryExpressionSyntax node) + void Advance(int count) { - var left = Visit(node.Left); - - var right = ConvertIfNeeded(Visit(node.Right), left.Type); - - return Expression.MakeBinary(ParseBinaryOperator(node.OperatorToken), left, right); + position += count; } - - public override Expression VisitMemberAccessExpression(MemberAccessExpressionSyntax node) + Token Peek(int offset = 0) { - 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) + if (position + offset >= tokens.Count) { - return parameter; + return new Token(TokenType.None, string.Empty); } - var type = GetType(node.Identifier.Text); - - if (type != null) - { - return Expression.Constant(type); - } - - throw new NotSupportedException("Unsupported identifier: " + node.Identifier.Text); + return tokens[position + offset]; } - public override Expression VisitConditionalExpression(ConditionalExpressionSyntax node) + private LambdaExpression ParseLambda(Type paramType) { - var condition = Visit(node.Condition); + var parameterIdentifier = Expect(TokenType.Identifier); - var whenTrue = Visit(node.WhenTrue); + var parameter = Expression.Parameter(paramType, parameterIdentifier.Value); - var whenFalse = Visit(node.WhenFalse); + parameterStack.Push(parameter); - if (whenTrue.Type != whenFalse.Type) - { - if (whenTrue.Type == typeof(object)) - { - whenTrue = Expression.Convert(whenTrue, whenFalse.Type); - } - else if (whenFalse.Type == typeof(object)) - { - whenFalse = Expression.Convert(whenFalse, whenTrue.Type); - } - else - { - throw new NotSupportedException("Conditional expression types mismatch: " + whenTrue.Type + " and " + whenFalse.Type); - } - } + Expect(TokenType.EqualsGreaterThan); - return Expression.Condition(condition, whenTrue, whenFalse); - } + var body = ParseExpression(parameter); - public override Expression VisitParenthesizedExpression(ParenthesizedExpressionSyntax node) - { - return Visit(node.Expression); - } - - private Type GetType(string typeName) - { - var nullable = typeName.EndsWith('?'); - - if (nullable) - { - typeName = typeName[..^1]; - } - - var type = 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(DateTimeOffset) => typeof(DateTimeOffset), - 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), - "bool" => typeof(bool), - _ => typeLocator?.Invoke(typeName) - }; - - if (nullable && type != null) - { - type = typeof(Nullable<>).MakeGenericType(type); - } - - return type; - } - - public override Expression VisitCastExpression(CastExpressionSyntax node) - { - var typeName = node.Type.ToString(); - - var targetType = GetType(typeName); - - 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); - } - - public override Expression VisitArrayCreationExpression(ArrayCreationExpressionSyntax node) - { - var elementType = GetType(node.Type.ElementType.ToString()); - - if (elementType == null) - { - throw new NotSupportedException("Unsupported array element type: " + node.Type.ElementType); - } - - var expressions = node.Initializer.Expressions.Select(e => ConvertIfNeeded(Visit(e), elementType)); - - return Expression.NewArrayInit(elementType, expressions); - } - - private static MethodCallExpression 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); + parameterStack.Pop(); return Expression.Lambda(body, parameter); } - private Expression VisitArgument(Expression instance, ArgumentSyntax argument) + private Expression ParseExpression(ParameterExpression parameter) { - if (argument.Expression is SimpleLambdaExpressionSyntax lambda) + var left = ParseBinary(parameter); + var token = Peek(); + + if (token.Type is TokenType.AmpersandAmpersand) { - var itemType = GetItemType(instance.Type); + Advance(1); - var visitor = new ExpressionSyntaxVisitor(Expression.Parameter(itemType, lambda.Parameter.Identifier.Text), typeLocator); + var right = ParseExpression(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}"); - return visitor.Visit(lambda); + left = Expression.AndAlso(left, right); + } + else if (token.Type is TokenType.BarBar) + { + Advance(1); + + var right = ParseExpression(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}"); + + left = Expression.OrElse(left, right); } - return Visit(argument.Expression); + return left; + } + + private Expression ParseBinary(ParameterExpression parameter) + { + var left = ParseNullCoalescing(parameter); + var token = Peek(); + + if (token.Type is TokenType.EqualsEquals or TokenType.NotEquals or TokenType.GreaterThan or TokenType.LessThan or TokenType.LessThanOrEqual or TokenType.GreaterThanOrEqual) + { + Advance(1); + var right = ParseBinary(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}"); + left = Expression.MakeBinary(token.Type.ToExpressionType(), left, ConvertIfNeeded(right, left.Type)); + } + + return left; + } + + private Expression ParseNullCoalescing(ParameterExpression parameter) + { + var left = ParseTernary(parameter); + var token = Peek(); + + while (token.Type == TokenType.QuestionMarkQuestionMark) + { + Advance(1); + + var right = ParseTernary(parameter) ?? throw new InvalidOperationException($"Expected expression after ?? at position {position}"); + + left = Expression.Coalesce(left, right); + + token = Peek(); + } + + return left; + } + + private Expression ParseTernary(ParameterExpression parameter) + { + var condition = ParseOr(parameter); + + if (Peek().Type == TokenType.QuestionMark) + { + Advance(1); + + var trueExpression = ParseOr(parameter); + + Expect(TokenType.Colon); + + var falseExpression = ParseOr(parameter); + + if (trueExpression is ConstantExpression trueConst && trueConst.Value == null && falseExpression is not ConstantExpression) + { + trueExpression = Expression.Constant(null, falseExpression.Type); + } + else if (falseExpression is ConstantExpression falseConst && falseConst.Value == null && trueExpression is not ConstantExpression) + { + falseExpression = Expression.Constant(null, trueExpression.Type); + } + + var ternary = Expression.Condition(condition, trueExpression, falseExpression); + + return ParseMemberAccess(ternary, parameter); + } + + return ParseMemberAccess(condition, parameter); + } + + private Expression ParseMemberAccess(Expression expression, ParameterExpression parameter) + { + var token = Peek(); + while (token.Type is TokenType.Dot or TokenType.QuestionDot or TokenType.OpenBracket) + { + if (token.Type == TokenType.Dot) + { + Advance(1); + token = Expect(TokenType.Identifier); + if (Peek().Type == TokenType.OpenParen) + { + expression = ParseInvocation(expression, token.Value, parameter); + } + else + { + expression = Expression.PropertyOrField(expression, token.Value); + } + } + else if (token.Type == TokenType.QuestionDot) + { + Advance(1); + token = Expect(TokenType.Identifier); + + var check = Expression.Equal(expression, Expression.Constant(null)); + + if (Peek().Type == TokenType.OpenParen) + { + var call = ParseInvocation(expression, token.Value, parameter); + expression = Expression.Condition(check, Expression.Constant(null, call.Type), call); + } + else + { + var access = Expression.PropertyOrField(expression, token.Value); + + expression = Expression.Condition(check, Expression.Default(access.Type), access); + + var nextToken = Peek(); + + if (nextToken.Type == TokenType.Dot || nextToken.Type == TokenType.QuestionDot) + { + var nextAccess = ParseMemberAccess(access, parameter); + + expression = Expression.Condition(check, Expression.Default(nextAccess.Type), nextAccess); + } + } + } + else if (token.Type == TokenType.OpenBracket) + { + Advance(1); + var index = ParseExpression(parameter); + Expect(TokenType.CloseBracket); + + if (expression.Type.IsArray) + { + expression = Expression.ArrayIndex(expression, index); + } + else + { + var indexer = expression.Type.GetProperty("Item") ?? throw new InvalidOperationException($"Type {expression.Type} does not have an indexer property"); + + expression = Expression.Property(expression, indexer, index); + } + } + + token = Peek(); + } + + return expression; + } + + private MethodCallExpression ParseInvocation(Expression expression, string methodName, ParameterExpression parameter) + { + Advance(1); + + var arguments = new List(); + + if (Peek().Type != TokenType.CloseParen) + { + while (Peek().Type != TokenType.CloseParen) + { + var token = Peek(); + + if (token.Type == TokenType.Identifier && Peek(1).Type == TokenType.EqualsGreaterThan) + { + var lambdaParameterName = token.Value; + + Advance(2); + + Type? lambdaParameterType = null; + + var extensionMethod = typeof(Enumerable).GetMethods(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(m => m.Name == methodName && m.GetParameters().Length == 2); + + if (extensionMethod != null) + { + lambdaParameterType = GetItemType(expression.Type); + } + + if (lambdaParameterType == null) + { + throw new InvalidOperationException($"Could not infer type for lambda parameter {lambdaParameterName}"); + } + + var lambdaParameter = Expression.Parameter(lambdaParameterType, lambdaParameterName); + parameterStack.Push(lambdaParameter); + var lambdaBody = ParseExpression(lambdaParameter); + parameterStack.Pop(); + arguments.Add(Expression.Lambda(lambdaBody, lambdaParameter)); + } + else + { + arguments.Add(ParseExpression(parameter)); + } + + if (Peek().Type == TokenType.Comma) + { + Advance(1); + } + } + } + + Expect(TokenType.CloseParen); + + var argumentTypes = arguments.Select(a => a.Type).ToArray(); + + var method = expression.Type.GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance, null, argumentTypes, null); + + if (method != null) + { + return Expression.Call(expression, method, arguments); + } + + method = typeof(Enumerable).GetMethods(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(m => m.Name == methodName && m.GetParameters().Length == arguments.Count + 1); + + if (method != null) + { + var argumentType = GetItemType(expression.Type); + + if (argumentType == null) + { + throw new InvalidOperationException($"Cannot determine item type for {expression.Type}"); + } + + if (method.IsGenericMethodDefinition) + { + method = method.MakeGenericMethod(argumentType); + } + + var parameters = method.GetParameters(); + + var argumentsWithInstance = new[] { expression }.Concat(arguments).ToArray(); + + return Expression.Call(method, argumentsWithInstance.Select((a, index) => ConvertIfNeeded(a, parameters[index].ParameterType))); + } + + throw new InvalidOperationException($"No suitable method '{methodName}' found for type '{expression.Type}'"); + } + + private static Type? GetItemType(Type enumerableOrArray) + { + return enumerableOrArray.IsArray ? enumerableOrArray.GetElementType() : enumerableOrArray.GetGenericArguments()[0]; + } + + private Expression? ParseTerm(ParameterExpression parameter) + { + var token = Peek(); + + if (token.Type == TokenType.None) + { + return null; + } + + if (token.Type == TokenType.OpenParen) + { + Advance(1); + + if (TryParseCastExpression(parameter, out var expression)) + { + return expression; + } + + expression = ParseExpression(parameter); + + Expect(TokenType.CloseParen); + + return expression; + } + + if (token.Type == TokenType.Identifier) + { + var matchingParameter = parameterStack.FirstOrDefault(p => p.Name == token.Value); + if (matchingParameter != null) + { + Advance(1); + return ParseMemberAccess(matchingParameter, parameter); + } + + var type = GetWellKnownType(token.Value); + + if (type != null) + { + Advance(1); + return ParseStaticMemberAccess(type, parameter); + } + + if (Peek(1).Type == TokenType.OpenParen) + { + Advance(1); + return ParseInvocation(parameter, token.Value, parameter); + } + + throw new InvalidOperationException($"Unexpected identifier: {token.Value}"); + } + + if (token.Type == TokenType.ExclamationMark) + { + Advance(1); + + var operand = ParseTerm(parameter) ?? throw new InvalidOperationException($"Expected expression after ! at position {position}"); + + operand = ConvertIfNeeded(operand, typeof(bool)); + + return Expression.Not(operand); + } + + if (token.Type == TokenType.Minus) + { + Advance(1); + + var operand = ParseTerm(parameter) ?? throw new InvalidOperationException($"Expected expression after - at position {position}"); + + return Expression.Negate(operand); + } + + if (token.Type == TokenType.Plus) + { + Advance(1); + + var operand = ParseTerm(parameter) ?? throw new InvalidOperationException($"Expected expression after + at position {position}"); + + return operand; + } + + switch (token.Type) + { + case TokenType.CharacterLiteral: + case TokenType.StringLiteral: + case TokenType.NullLiteral: + case TokenType.NumericLiteral: + case TokenType.TrueLiteral: + case TokenType.FalseLiteral: + Advance(1); + return token.ToConstantExpression(); + case TokenType.New: + Advance(1); + + token = Peek(); + + if (token.Type == TokenType.OpenBrace) + { + Advance(1); + + var properties = new List<(string Name, Expression Expression)>(); + + if (Peek().Type != TokenType.CloseBrace) + { + do + { + token = Peek(); + string propertyName; + Expression propertyExpression; + + if (token.Type == TokenType.Identifier) + { + propertyName = token.Value; + Advance(1); + if (Peek().Type == TokenType.Dot || Peek().Type == TokenType.QuestionDot) + { + // Handle nested property access + Expression expr = propertyName == parameter.Name ? (Expression)parameter : Expression.Property(parameter, propertyName); + propertyExpression = ParseMemberAccess(expr, parameter); + + // Get the last identifier token's value + var lastToken = tokens[position - 1]; + if (lastToken.Type == TokenType.Identifier) + { + propertyName = lastToken.Value; + } + } + else + { + Expect(TokenType.Equals); + propertyExpression = ParseExpression(parameter); + } + } + else + { + propertyExpression = ParseExpression(parameter); + + if (propertyExpression is MemberExpression memberExpression) + { + propertyName = memberExpression.Member.Name; + } + else + { + throw new InvalidOperationException($"Invalid anonymous type member expression at position {position}"); + } + } + + properties.Add((propertyName, propertyExpression)); + + if (Peek().Type == TokenType.Comma) + { + Advance(1); + } + else + { + break; + } + } while (Peek().Type != TokenType.CloseBrace); + } + + Expect(TokenType.CloseBrace); + + var propertyTypes = properties.Select(p => p.Expression.Type).ToArray(); + var propertyNames = properties.Select(p => p.Name).ToArray(); + var dynamicType = DynamicTypeFactory.CreateType(parameter.Type.Name, propertyNames, propertyTypes); + var bindings = properties.Select(p => Expression.Bind(dynamicType.GetProperty(p.Name)!, p.Expression)); + return Expression.MemberInit(Expression.New(dynamicType), bindings); + } + else + { + Type? elementType = null; + var nullable = false; + + if (token.Type == TokenType.Identifier) + { + var typeName = token.Value; + elementType = GetWellKnownType(typeName); + Advance(1); + + if (Peek().Type == TokenType.QuestionMark) + { + nullable = true; + Advance(1); + } + } + + Expect(TokenType.OpenBracket); + Expect(TokenType.CloseBracket); + Expect(TokenType.OpenBrace); + + var elements = new List(); + if (Peek().Type != TokenType.CloseBrace) + { + do + { + elements.Add(ParseExpression(parameter)); + if (Peek().Type == TokenType.Comma) + { + Advance(1); + } + else + { + break; + } + } while (Peek().Type != TokenType.CloseBrace); + } + + Expect(TokenType.CloseBrace); + + if (elementType == null) + { + elementType = elements.Count > 0 ? elements[0].Type : typeof(object); + } + + if (nullable) + { + elementType = typeof(Nullable<>).MakeGenericType(elementType); + } + + return Expression.NewArrayInit(elementType, elements.Select(e => ConvertIfNeeded(e, elementType))); + } + default: + throw new InvalidOperationException($"Unexpected token: {token.Type} at position {position}"); + } + } + + private bool TryParseCastExpression(ParameterExpression parameter, out Expression expression) + { + expression = null!; + + var token = Peek(); + + if (token.Type != TokenType.Identifier) + { + return false; + } + + var typeName = new StringBuilder(token.Value); + var index = position + 1; + var typeCast = true; + var nullable = false; + + while (index < tokens.Count) + { + token = tokens[index]; + + if (token.Type == TokenType.Dot) + { + index++; + if (index >= tokens.Count || tokens[index].Type != TokenType.Identifier) + { + typeCast = false; + break; + } + typeName.Append('.').Append(tokens[index].Value); + index++; + } + else if (token.Type == TokenType.QuestionMark) + { + nullable = true; + index++; + if (index >= tokens.Count || tokens[index].Type != TokenType.CloseParen) + { + typeCast = false; + break; + } + } + else if (token.Type == TokenType.CloseParen) + { + break; + } + else + { + typeCast = false; + break; + } + } + + if (typeCast && index < tokens.Count && tokens[index].Type == TokenType.CloseParen) + { + var name = typeName.ToString(); + + var type = GetWellKnownType(name) ?? typeResolver?.Invoke(name) ?? throw new InvalidOperationException($"Could not resolve type: {typeName}"); + + if (nullable && type.IsValueType) + { + type = typeof(Nullable<>).MakeGenericType(type); + } + + position = index; + + Advance(1); + + if (Peek().Type == TokenType.OpenParen && TryParseCastExpression(parameter, out var innerExpression)) + { + expression = Expression.Convert(innerExpression, type); + } + else + { + var source = ParseTerm(parameter) ?? throw new InvalidOperationException($"Expected expression to cast at position {position}"); + expression = Expression.Convert(source, type); + } + + return true; + } + + return false; + } + + private Expression ParseStaticMemberAccess(Type type, ParameterExpression parameter) + { + Expect(TokenType.Dot); + + var token = Expect(TokenType.Identifier); + + if (Peek().Type == TokenType.OpenParen) + { + return ParseStaticInvocation(type, token.Value, parameter); + } + else + { + var member = (MemberInfo?)type.GetProperty(token.Value) ?? type.GetField(token.Value); + + if (member == null) + { + throw new InvalidOperationException($"Member {token.Value} not found on type {type.Name}"); + } + + return Expression.MakeMemberAccess(null, member); + } + + throw new InvalidOperationException($"Expected method invocation after {token.Value} at position {position}"); + } + + private Expression ParseStaticInvocation(Type type, string methodName, ParameterExpression parameter) + { + Advance(1); + + var arguments = new List(); + + if (Peek().Type != TokenType.CloseParen) + { + arguments.Add(ParseExpression(parameter)); + + while (Peek().Type == TokenType.Comma) + { + Advance(1); + arguments.Add(ParseExpression(parameter)); + } + } + + Expect(TokenType.CloseParen); + + var method = type.GetMethod(methodName, [.. arguments.Select(a => a.Type)]) ?? throw new InvalidOperationException($"Method {methodName} not found on type {type.Name}"); + + return Expression.Call(null, method, arguments); + } + + private static Type? GetWellKnownType(string typeName) + { + return typeName switch + { + nameof(DateTime) => typeof(DateTime), + nameof(DateOnly) => typeof(DateOnly), + nameof(TimeOnly) => typeof(TimeOnly), + nameof(DateTimeOffset) => typeof(DateTimeOffset), + nameof(Guid) => typeof(Guid), + nameof(CultureInfo) => typeof(CultureInfo), + nameof(Double) or "double" => typeof(double), + nameof(Single) or "float" => typeof(float), + nameof(Int32) or "int" => typeof(int), + nameof(Int64) or "long" => typeof(long), + nameof(Int16) or "short" => typeof(short), + nameof(Byte) or "byte" => typeof(byte), + nameof(SByte) or "sbyte" => typeof(sbyte), + nameof(UInt32) or "uint" => typeof(uint), + nameof(UInt64) or "ulong" => typeof(ulong), + nameof(UInt16) or "ushort" => typeof(ushort), + nameof(Boolean) or "bool" => typeof(bool), + nameof(Char) or "char" => typeof(char), + nameof(Decimal) or "decimal" => typeof(decimal), + nameof(String) or "string" => typeof(string), + nameof(Math) => typeof(Math), + nameof(Convert) => typeof(Convert), + _ => null + }; + } + + private Expression ParseOr(ParameterExpression parameter) + { + var left = ParseAnd(parameter); + + var token = Peek(); + while (token.Type == TokenType.BarBar) + { + Advance(1); + var right = ParseAnd(parameter) ?? throw new InvalidOperationException($"Expected expression after || at position {position}"); + left = Expression.OrElse(left, right); + token = Peek(); + } + + return left; + } + + private Expression ParseAnd(ParameterExpression parameter) + { + var left = ParseComparison(parameter); + + var token = Peek(); + while (token.Type == TokenType.AmpersandAmpersand) + { + Advance(1); + var right = ParseComparison(parameter) ?? throw new InvalidOperationException($"Expected expression after && at position {position}"); + left = Expression.AndAlso(left, right); + token = Peek(); + } + + return left; + } + + private Expression ParseComparison(ParameterExpression parameter) + { + var left = ParseShift(parameter); + + var token = Peek(); + if (token.Type is TokenType.EqualsEquals or TokenType.NotEquals or TokenType.GreaterThan or TokenType.LessThan or TokenType.LessThanOrEqual or TokenType.GreaterThanOrEqual) + { + Advance(1); + var right = ParseShift(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}"); + left = Expression.MakeBinary(token.Type.ToExpressionType(), left, ConvertIfNeeded(right, left.Type)); + } + + return ParseBinaryAnd(left, parameter); + } + + private Expression ParseBinaryAnd(Expression left, ParameterExpression parameter) + { + var token = Peek(); + while (token.Type == TokenType.Ampersand) + { + Advance(1); + var right = ParseShift(parameter) ?? throw new InvalidOperationException($"Expected expression after & at position {position}"); + left = Expression.MakeBinary(ExpressionType.And, left, ConvertIfNeeded(right, left.Type)); + token = Peek(); + } + + return ParseBinaryXor(left, parameter); + } + + private Expression ParseBinaryXor(Expression left, ParameterExpression parameter) + { + var token = Peek(); + while (token.Type == TokenType.Caret) + { + Advance(1); + var right = ParseBinaryAnd(ParseShift(parameter), parameter) ?? throw new InvalidOperationException($"Expected expression after ^ at position {position}"); + left = Expression.MakeBinary(ExpressionType.ExclusiveOr, left, ConvertIfNeeded(right, left.Type)); + token = Peek(); + } + + return ParseBinaryOr(left, parameter); + } + + private Expression ParseBinaryOr(Expression left, ParameterExpression parameter) + { + var token = Peek(); + while (token.Type == TokenType.Bar) + { + Advance(1); + var right = ParseBinaryXor(ParseShift(parameter), parameter) ?? throw new InvalidOperationException($"Expected expression after | at position {position}"); + left = Expression.MakeBinary(ExpressionType.Or, left, ConvertIfNeeded(right, left.Type)); + token = Peek(); + } + + return left; + } + + private Expression ParseShift(ParameterExpression parameter) + { + var left = ParseAdditive(parameter); + + var token = Peek(); + while (token.Type is TokenType.LessThanLessThan or TokenType.GreaterThanGreaterThan) + { + Advance(1); + var right = ParseAdditive(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}"); + left = Expression.MakeBinary(token.Type.ToExpressionType(), left, ConvertIfNeeded(right, left.Type)); + token = Peek(); + } + + return left; + } + + private Expression ParseAdditive(ParameterExpression parameter) + { + var left = ParseMultiplicative(parameter); + + var token = Peek(); + while (token.Type is TokenType.Plus or TokenType.Minus) + { + Advance(1); + var right = ParseMultiplicative(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}"); + + if (token.Type == TokenType.Plus && left.Type == typeof(string)) + { + left = Expression.Call(null, typeof(string).GetMethod(nameof(string.Concat), [typeof(string), typeof(string)])!, left, ConvertIfNeeded(right, typeof(string))); + } + else + { + left = Expression.MakeBinary(token.Type.ToExpressionType(), left, ConvertIfNeeded(right, left.Type)); + } + + token = Peek(); + } + + return left; + } + + private Expression ParseMultiplicative(ParameterExpression parameter) + { + var left = ParseTerm(parameter) ?? throw new InvalidOperationException($"Expected expression at position {position}"); + + var token = Peek(); + while (token.Type is TokenType.Star or TokenType.Slash) + { + Advance(1); + var right = ParseTerm(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}"); + left = Expression.MakeBinary(token.Type.ToExpressionType(), left, ConvertIfNeeded(right, left.Type)); + token = Peek(); + } + + return left; } private static Expression ConvertIfNeeded(Expression expression, Type targetType) @@ -270,279 +909,4 @@ class ExpressionSyntaxVisitor : CSharpSyntaxVisitor 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 argumentType = GetItemType(instanceType); - var genericMethod = methodInfo.MakeGenericMethod(argumentType); - var parameters = genericMethod.GetParameters(); - var argumentsWithInstance = new[] { instance }.Concat(arguments).ToArray(); - - if (parameters.Length != argumentsWithInstance.Length) - { - throw new NotSupportedException("Unsupported method call: " + methodCall.Name.Identifier.Text); - } - - return Expression.Call(genericMethod, argumentsWithInstance.Select((a, index) => ConvertIfNeeded(a, parameters[index].ParameterType))); - } - } - - 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, - SyntaxKind.QuestionQuestionToken => ExpressionType.Coalesce, - _ => throw new NotSupportedException("Unsupported operator: " + token.Text), - }; - } - - private static string GetPropertyNameFromInitializer(AnonymousObjectMemberDeclaratorSyntax initializer) - { - if (initializer.NameEquals != null) - { - return initializer.NameEquals.Name.Identifier.Text; - } - - var expression = initializer.Expression; - - if (expression is MemberAccessExpressionSyntax memberAccess) - { - expression = memberAccess.Name; - } - - while (expression is ConditionalAccessExpressionSyntax conditionalAccess) - { - expression = conditionalAccess.WhenNotNull; - } - - if (expression is MemberBindingExpressionSyntax memberBinding) - { - expression = memberBinding.Name; - } - - if (expression is IdentifierNameSyntax identifier) - { - return identifier.Identifier.Text; - } - - throw new NotSupportedException("Unsupported initializer: " + initializer.ToString()); - } - - public override Expression VisitAnonymousObjectCreationExpression(AnonymousObjectCreationExpressionSyntax node) - { - var properties = node.Initializers.Select(init => - { - var name = GetPropertyNameFromInitializer(init); - 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(parameter.Type.Name, propertyNames, propertyTypes); - - var bindings = properties.Select(p => Expression.Bind(dynamicType.GetProperty(p.Name), p.Value)); - return Expression.MemberInit(Expression.New(dynamicType), bindings); - } - - private Expression instance; - - public override Expression VisitConditionalAccessExpression(ConditionalAccessExpressionSyntax node) - { - var expression = Visit(node.Expression); - - instance = expression; - - var whenNotNull = Visit(node.WhenNotNull); - - instance = null; - - 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 (instance is Expression expression) - { - return Expression.PropertyOrField(expression, node.Name.Identifier.Text); - } - - 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 override Expression VisitPrefixUnaryExpression(PrefixUnaryExpressionSyntax node) - { - var operand = Visit(node.Operand); - - return node.OperatorToken.Kind() switch - { - SyntaxKind.MinusToken => Expression.Negate(operand), - SyntaxKind.ExclamationToken => Expression.Not(operand), - _ => throw new NotSupportedException("Unsupported unary operator: " + node.OperatorToken.Text), - }; - } -} - -/// -/// Parse lambda expressions from strings. -/// -public static class ExpressionParser -{ - /// - /// Parses a lambda expression that returns a boolean value. - /// - public static Expression> ParsePredicate(string expression, Func typeLocator = null) - { - return ParseLambda(expression, typeLocator); - } - - /// - /// Parses a lambda expression that returns a typed result. - /// - public static Expression> ParseLambda(string expression, Func typeLocator = null) - { - var (parameter, body) = Parse(expression, typeLocator); - - return Expression.Lambda>(body, parameter); - } - - - private static (ParameterExpression, Expression) Parse(string expression, Func typeLocator) - { - var syntaxTree = CSharpSyntaxTree.ParseText(expression); - var root = syntaxTree.GetRoot(); - var lambdaExpression = root.DescendantNodes().OfType().FirstOrDefault(); - if (lambdaExpression == null) - { - throw new ArgumentException("Invalid lambda expression."); - } - var parameter = Expression.Parameter(typeof(T), lambdaExpression.Parameter.Identifier.Text); - var visitor = new ExpressionSyntaxVisitor(parameter, typeLocator); - var body = visitor.Visit(lambdaExpression.Body); - return (parameter, body); - } - - /// - /// Parses a lambda expression that returns untyped result. - /// - public static LambdaExpression ParseLambda(string expression, Func typeLocator = null) - { - var (parameter, body) = Parse(expression, typeLocator); - - return Expression.Lambda(body, parameter); - } - - /// - /// Parses a lambda expression that returns untyped result. - /// - public static LambdaExpression ParseLambda(string expression, Type type, Func typeLocator = null) - { - var syntaxTree = CSharpSyntaxTree.ParseText(expression); - var root = syntaxTree.GetRoot(); - var lambdaExpression = root.DescendantNodes().OfType().FirstOrDefault(); - - if (lambdaExpression == null) - { - throw new ArgumentException("Invalid lambda expression."); - } - - var parameter = Expression.Parameter(type, lambdaExpression.Parameter.Identifier.Text); - var visitor = new ExpressionSyntaxVisitor(parameter, typeLocator); - var body = visitor.Visit(lambdaExpression.Body); - - return Expression.Lambda(body, parameter); - } -} +} \ No newline at end of file diff --git a/Radzen.Blazor/ExpressionSerializer.cs b/Radzen.Blazor/ExpressionSerializer.cs new file mode 100644 index 000000000..802d69c54 --- /dev/null +++ b/Radzen.Blazor/ExpressionSerializer.cs @@ -0,0 +1,462 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; + +/// +/// Serializes LINQ Expression Trees into C# string representations. +/// +public class ExpressionSerializer : ExpressionVisitor +{ + private readonly StringBuilder _sb = new StringBuilder(); + + /// + /// Serializes a given LINQ Expression into a C# string. + /// + /// The expression to serialize. + /// A string representation of the expression. + public string Serialize(Expression expression) + { + _sb.Clear(); + Visit(expression); + return _sb.ToString(); + } + + /// + protected override Expression VisitLambda(Expression node) + { + if (node.Parameters.Count > 1) + { + _sb.Append("("); + for (int i = 0; i < node.Parameters.Count; i++) + { + if (i > 0) _sb.Append(", "); + _sb.Append(node.Parameters[i].Name); + } + _sb.Append(") => "); + } + else + { + _sb.Append(node.Parameters[0].Name); + _sb.Append(" => "); + } + Visit(node.Body); + return node; + } + + /// + protected override Expression VisitParameter(ParameterExpression node) + { + _sb.Append(node.Name); + return node; + } + + /// + protected override Expression VisitMember(MemberExpression node) + { + if (node.Expression != null) + { + Visit(node.Expression); + _sb.Append($".{node.Member.Name}"); + } + else + { + _sb.Append(node.Member.Name); + } + return node; + } + + /// + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.IsStatic && node.Arguments.Count > 0 && + (node.Method.DeclaringType == typeof(Enumerable) || + node.Method.DeclaringType == typeof(Queryable))) + { + Visit(node.Arguments[0]); + _sb.Append($".{node.Method.Name}("); + + for (int i = 1; i < node.Arguments.Count; i++) + { + if (i > 1) _sb.Append(", "); + + if (node.Arguments[i] is NewArrayExpression arrayExpr) + { + VisitNewArray(arrayExpr); + } + else + { + Visit(node.Arguments[i]); + } + } + + _sb.Append(")"); + } + else if (node.Method.IsStatic) + { + _sb.Append($"{node.Method.DeclaringType.Name}.{node.Method.Name}("); + + for (int i = 0; i < node.Arguments.Count; i++) + { + if (i > 0) _sb.Append(", "); + Visit(node.Arguments[i]); + } + + _sb.Append(")"); + } + else + { + if (node.Object != null) + { + Visit(node.Object); + _sb.Append($".{node.Method.Name}("); + } + else + { + _sb.Append($"{node.Method.Name}("); + } + + for (int i = 0; i < node.Arguments.Count; i++) + { + if (i > 0) _sb.Append(", "); + Visit(node.Arguments[i]); + } + + _sb.Append(")"); + } + + return node; + } + + /// + protected override Expression VisitUnary(UnaryExpression node) + { + if (node.NodeType == ExpressionType.Not) + { + _sb.Append("(!"); + Visit(node.Operand); + _sb.Append(")"); + } + else if (node.NodeType == ExpressionType.Convert) + { + if (node.Operand is IndexExpression indexExpr) + { + _sb.Append($"({node.Type.DisplayName(true).Replace("+",".")})"); + + Visit(indexExpr.Object); + + _sb.Append("["); + Visit(indexExpr.Arguments[0]); + _sb.Append("]"); + + return node; + } + + Visit(node.Operand); + } + else + { + _sb.Append(node.NodeType switch + { + ExpressionType.Negate => "-", + ExpressionType.UnaryPlus => "+", + _ => throw new NotSupportedException($"Unsupported unary operator: {node.NodeType}") + }); + Visit(node.Operand); + } + return node; + } + + /// + protected override Expression VisitConstant(ConstantExpression node) + { + _sb.Append(FormatValue(node.Value)); + return node; + } + + private string FormatValue(object value) + { + if (value == null) + return "null"; + + return value switch + { + string str => $"\"{str}\"", + char c => $"'{c}'", + bool b => b.ToString().ToLowerInvariant(), + DateTime dt => $"DateTime.Parse(\"{dt.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture)}\", CultureInfo.InvariantCulture)", + DateOnly dateOnly => $"DateOnly.Parse(\"{dateOnly.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}\", CultureInfo.InvariantCulture)", + TimeOnly timeOnly => $"TimeOnly.Parse(\"{timeOnly.ToString("HH:mm:ss", CultureInfo.InvariantCulture)}\", CultureInfo.InvariantCulture)", + Guid guid => $"Guid.Parse(\"{guid.ToString("D", CultureInfo.InvariantCulture)}\")", + IEnumerable enumerable when value is not string => FormatEnumerable(enumerable), + _ => value.GetType().IsEnum + ? $"({value.GetType().FullName.Replace("+", ".")})" + Convert.ChangeType(value, Enum.GetUnderlyingType(value.GetType()), CultureInfo.InvariantCulture).ToString() + : Convert.ToString(value, CultureInfo.InvariantCulture) + }; + } + + private string FormatEnumerable(IEnumerable enumerable) + { + var arrayType = enumerable.AsQueryable().ElementType; + + var items = enumerable.Cast().Select(FormatValue); + return $"new {(Nullable.GetUnderlyingType(arrayType) != null ? arrayType.DisplayName(true).Replace("+", ".") : "")}[] {{ {string.Join(", ", items)} }}"; + } + + /// + protected override Expression VisitNewArray(NewArrayExpression node) + { + bool needsParentheses = node.NodeType == ExpressionType.NewArrayInit && + (node.Expressions.Count > 1 || node.Expressions[0].NodeType != ExpressionType.Constant); + + if (needsParentheses) _sb.Append("("); + + _sb.Append("new [] { "); + bool first = true; + foreach (var expr in node.Expressions) + { + if (!first) _sb.Append(", "); + first = false; + Visit(expr); + } + _sb.Append(" }"); + + if (needsParentheses) _sb.Append(")"); + + return node; + } + + /// + protected override Expression VisitBinary(BinaryExpression node) + { + _sb.Append("("); + Visit(node.Left); + _sb.Append($" {GetOperator(node.NodeType)} "); + Visit(node.Right); + _sb.Append(")"); + return node; + } + + /// + protected override Expression VisitConditional(ConditionalExpression node) + { + _sb.Append("("); + Visit(node.Test); + _sb.Append(" ? "); + Visit(node.IfTrue); + _sb.Append(" : "); + Visit(node.IfFalse); + _sb.Append(")"); + return node; + } + + /// + /// Maps an ExpressionType to its corresponding C# operator. + /// + /// The ExpressionType to map. + /// A string representation of the corresponding C# operator. + private static string GetOperator(ExpressionType type) + { + return type switch + { + ExpressionType.Add => "+", + ExpressionType.Subtract => "-", + ExpressionType.Multiply => "*", + ExpressionType.Divide => "/", + ExpressionType.AndAlso => "&&", + ExpressionType.OrElse => "||", + ExpressionType.Equal => "==", + ExpressionType.NotEqual => "!=", + ExpressionType.LessThan => "<", + ExpressionType.LessThanOrEqual => "<=", + ExpressionType.GreaterThan => ">", + ExpressionType.GreaterThanOrEqual => ">=", + ExpressionType.Coalesce => "??", + _ => throw new NotSupportedException($"Unsupported operator: {type}") + }; + } +} + +/// +/// Provides an extension method for displaying type names. +/// +public static class SharedTypeExtensions +{ + private static readonly Dictionary 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" } + }; + + /// + /// Unwraps nullable type. + /// + public static Type UnwrapNullableType(this Type type) + => Nullable.GetUnderlyingType(type) ?? type; + + /// + /// Returns a display name for the given type. + /// + /// The type to display. + /// Indicates whether to use the full name. + /// Indicates whether to use a compilable format. + /// A string representing the type name. + 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('>'); + } +} \ No newline at end of file diff --git a/Radzen.Blazor/GaugeBase.cs b/Radzen.Blazor/GaugeBase.cs index cb5d5366f..990383b18 100644 --- a/Radzen.Blazor/GaugeBase.cs +++ b/Radzen.Blazor/GaugeBase.cs @@ -187,7 +187,7 @@ namespace Radzen.Blazor if (Visible) { - JSRuntime.InvokeVoidAsync("Radzen.destroyGauge", Element); + JSRuntime.InvokeVoid("Radzen.destroyGauge", Element); } } diff --git a/Radzen.Blazor/JSRuntimeExtensions.cs b/Radzen.Blazor/JSRuntimeExtensions.cs new file mode 100644 index 000000000..61b5cf2a4 --- /dev/null +++ b/Radzen.Blazor/JSRuntimeExtensions.cs @@ -0,0 +1,24 @@ +using System; +using System.Threading.Tasks; +using Microsoft.JSInterop; + +namespace Radzen; + +static class JSRuntimeExtensions +{ + public static void InvokeVoid(this IJSRuntime jsRuntime, string identifier, params object[] args) + { + _ = jsRuntime.InvokeVoidAsync(identifier, args).FireAndForget(); + } + + private static async ValueTask FireAndForget(this ValueTask task) + { + try + { + await task; + } + catch (Exception) + { + } + } +} \ No newline at end of file diff --git a/Radzen.Blazor/NonRenderingExtensions.cs b/Radzen.Blazor/NonRenderingExtensions.cs new file mode 100644 index 000000000..25d1c4422 --- /dev/null +++ b/Radzen.Blazor/NonRenderingExtensions.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; + +namespace Radzen.Blazor; + +static class NonRenderingExtensions +{ + public static Action AsNonRenderingEventHandler(this ComponentBase _, Action callback) + => new SyncReceiver(callback).Invoke; + public static Action AsNonRenderingEventHandler(this Action callback) => new SyncReceiver(callback).Invoke; + public static Func AsNonRenderingEventHandler(this ComponentBase _, Func callback) => new AsyncReceiver(callback).Invoke; + public static Func AsNonRenderingEventHandler(this ComponentBase _, Func callback) => new AsyncReceiver(callback).Invoke; + + private record SyncReceiver(Action callback) : ReceiverBase + { + public void Invoke() => callback(); + } + + private record SyncReceiver(Action callback) : ReceiverBase + { + public void Invoke(T arg) => callback(arg); + } + + private record AsyncReceiver(Func callback) : ReceiverBase + { + public Task Invoke() => callback(); + } + + private record AsyncReceiver(Func callback) : ReceiverBase + { + public Task Invoke(T arg) => callback(arg); + } + + private record ReceiverBase : IHandleEvent + { + public Task HandleEventAsync(EventCallbackWorkItem item, object arg) => item.InvokeAsync(arg); + } +} \ No newline at end of file diff --git a/Radzen.Blazor/QueryableExtension.cs b/Radzen.Blazor/QueryableExtension.cs index 5fe694b6e..9b9703439 100644 --- a/Radzen.Blazor/QueryableExtension.cs +++ b/Radzen.Blazor/QueryableExtension.cs @@ -71,7 +71,7 @@ namespace Radzen var parameter = Expression.Parameter(source.ElementType, "x"); return GroupByMany(source, - properties.Select(p => Expression.Lambda>(Expression.Convert(GetNestedPropertyExpression(parameter, p), typeof(object)), parameter).Compile()).ToArray(), + properties.Select(p => Expression.Lambda>(Expression.Convert(GetNestedPropertyExpression(parameter, p), typeof(object)), parameter).Compile()).ToArray(), 0); } @@ -197,8 +197,8 @@ namespace Radzen /// An that contains each element of the source sequence converted to the specified type. public static IQueryable Cast(this IQueryable source, Type type) { - return source.Provider.CreateQuery(Expression.Call(null, - typeof(Queryable).GetTypeInfo().GetDeclaredMethods(nameof(Queryable.Cast)).FirstOrDefault(mi => mi.IsGenericMethod).MakeGenericMethod(type), + return source.Provider.CreateQuery(Expression.Call(null, + typeof(Queryable).GetTypeInfo().GetDeclaredMethods(nameof(Queryable.Cast)).FirstOrDefault(mi => mi.IsGenericMethod).MakeGenericMethod(type), source.Expression)); } @@ -234,7 +234,7 @@ namespace Radzen /// /// Filters using the specified filter descriptors. /// - public static IQueryable Where(this IQueryable source, IEnumerable filters, + public static IQueryable Where(this IQueryable source, IEnumerable filters, LogicalFilterOperator logicalFilterOperator, FilterCaseSensitivity filterCaseSensitivity) { if (filters == null || !filters.Any()) @@ -519,35 +519,6 @@ namespace Radzen return (IList)genericToList.Invoke(null, new[] { query }); } - static string EnumerableAsString(IQueryable enumerableValue, string baseType, object value) - { - Func> values = (items) => { - if (items.ElementType == typeof(string)) - { - return items.Cast().Select(i => $@"""{i}"""); - } - else if (items.ElementType == typeof(bool) || items.ElementType == typeof(bool?)) - { - return items.Cast().Select(i => $@"{i}".ToLower()); - } - else if (PropertyAccess.IsDate(items.ElementType)) - { - return items.Cast().Select(i => $@"DateTime.Parse(""{i}"")"); - } - else if (PropertyAccess.IsEnum(items.ElementType) || PropertyAccess.IsNullableEnum(items.ElementType)) - { - return items.Cast().Select(i => i != null ? Convert.ChangeType(i, typeof(int)) : null); - } - - return items.Cast(); - - }; - - var finalValues = value != null && !(IsEnumerable(value.GetType()) && !(value is string)) ? new object[] { value }.AsQueryable().Cast(value.GetType()) : enumerableValue; - - return "new " + baseType + "[]{" + String.Join(",", values(finalValues)) + "}"; - } - /// /// Converts to filterstring. /// @@ -556,203 +527,78 @@ namespace Radzen /// System.String. public static string ToFilterString(this IEnumerable> columns) { - var columnsWithFilter = GetFilterableColumns(columns); + Func, bool> canFilter = (c) => c.Filterable && c.FilterPropertyType != null && + (!(c.GetFilterValue() == null || c.GetFilterValue() as string == string.Empty) + || c.GetFilterOperator() == FilterOperator.IsNotNull || c.GetFilterOperator() == FilterOperator.IsNull + || c.GetFilterOperator() == FilterOperator.IsEmpty || c.GetFilterOperator() == FilterOperator.IsNotEmpty) + && c.GetFilterProperty() != null; - if (columnsWithFilter.Any()) + Func, bool> canFilterCustom = (c) => c.Filterable && c.FilterPropertyType != null && + (c.GetFilterOperator() == FilterOperator.Custom && c.GetCustomFilterExpression() != null) + && c.GetFilterProperty() != null; + + var columnsToFilter = columns.Where(canFilter); + var grid = columns.FirstOrDefault()?.Grid; + var gridLogicalFilterOperator = grid != null ? grid.LogicalFilterOperator : LogicalFilterOperator.And; + var gridFilterCaseSensitivity = grid != null ? grid.FilterCaseSensitivity : FilterCaseSensitivity.Default; + + var serializer = new ExpressionSerializer(); + var filterExpression = ""; + + if (columnsToFilter.Any()) { - var gridLogicalFilterOperator = columns.FirstOrDefault()?.Grid?.LogicalFilterOperator; - var gridBooleanOperator = gridLogicalFilterOperator == LogicalFilterOperator.And ? "&&" : "||"; - - var whereList = new List(); - - foreach (var column in columnsWithFilter) + var filters = columnsToFilter.Select(c => new FilterDescriptor() { - string value = ""; - string secondValue = ""; + Property = c.Property, + FilterProperty = c.FilterProperty, + Type = c.FilterPropertyType, + FilterValue = c.GetFilterValue(), + FilterOperator = c.GetFilterOperator(), + SecondFilterValue = c.GetSecondFilterValue(), + SecondFilterOperator = c.GetSecondFilterOperator(), + LogicalFilterOperator = c.GetLogicalFilterOperator() + }); - var v = column.GetFilterValue(); - var sv = column.GetSecondFilterValue(); + if (filters.Any()) + { + var parameter = Expression.Parameter(typeof(T), "x"); + Expression combinedExpression = null; - if (column.GetFilterOperator() == FilterOperator.Custom) + foreach (var filter in filters) { - var customFilterExpression = column.GetCustomFilterExpression(); - if (!string.IsNullOrEmpty(customFilterExpression)) - { - whereList.Add(customFilterExpression); - } - } - else if (v != null && IsEnumerable(v.GetType()) && v.GetType() != typeof(string) || IsEnumerable(column.FilterPropertyType) && column.FilterPropertyType != typeof(string)) - { - var enumerableValue = ((IEnumerable)(v != null ? v : Enumerable.Empty())).AsQueryable(); - var enumerableSecondValue = ((IEnumerable)(sv != null ? sv : Enumerable.Empty())).AsQueryable(); + var expression = GetExpression(parameter, filter, gridFilterCaseSensitivity, filter.Type); + if (expression == null) continue; - - string baseType = column.FilterPropertyType.GetGenericArguments().Count() == 1 - ? column.FilterPropertyType.GetGenericArguments()[0].Name - : ""; - - if (column.Property != column.FilterProperty) - { - baseType = ""; - } - - var enumerableValueAsString = EnumerableAsString(enumerableValue, baseType, v); - - var enumerableSecondValueAsString = EnumerableAsString(enumerableSecondValue, baseType, sv); - - if (enumerableValue?.Cast().Any() == true) - { - var columnFilterOperator = column.GetFilterOperator(); - var columnSecondFilterOperator = column.GetSecondFilterOperator(); - var linqOperator = LinqFilterOperators[column.GetFilterOperator()]; - if (linqOperator == null) - { - linqOperator = "=="; - } - - var booleanOperator = column.LogicalFilterOperator == LogicalFilterOperator.And ? "&&" : "||"; - - 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) - { - if (column.GetFilterValue() is string && column.Property != column.FilterProperty && !string.IsNullOrEmpty(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) - { - if (IsEnumerable(column.FilterPropertyType) && column.FilterPropertyType != typeof(string) && - IsEnumerable(column.PropertyType) && column.PropertyType != typeof(string)) - { - whereList.Add($@"{(columnFilterOperator == FilterOperator.NotIn ? "!" : "")}{property}.Any(i => ({enumerableValueAsString}).Contains(i))"); - } - else if (IsEnumerable(column.FilterPropertyType) && column.FilterPropertyType != typeof(string) && - column.Property != column.FilterProperty && !string.IsNullOrEmpty(column.FilterProperty)) - { - whereList.Add($@"{(columnFilterOperator == FilterOperator.NotIn ? "!" : "")}{itemInstanceName + column.Property}.Any(i => ({enumerableValueAsString}).Contains(i.{column.FilterProperty}))"); - } - } - } - else - { - if ((columnFilterOperator == FilterOperator.Contains || columnFilterOperator == FilterOperator.DoesNotContain) && - (columnSecondFilterOperator == FilterOperator.Contains || columnSecondFilterOperator == FilterOperator.DoesNotContain)) - { - whereList.Add($@"{(columnFilterOperator == FilterOperator.DoesNotContain ? "!" : "")}({enumerableValueAsString}).Contains({property}) {booleanOperator} {(columnSecondFilterOperator == FilterOperator.DoesNotContain ? "!" : "")}({enumerableSecondValueAsString}).Contains({property})"); - } - else if ((columnFilterOperator == FilterOperator.In || columnFilterOperator == FilterOperator.NotIn) && - (columnSecondFilterOperator == FilterOperator.In || columnSecondFilterOperator == FilterOperator.NotIn)) - { - whereList.Add($@"({property}).{(columnFilterOperator == FilterOperator.NotIn ? "Except" : "Intersect")}({enumerableValueAsString}).Any() {booleanOperator} ({property}).{(columnSecondFilterOperator == FilterOperator.NotIn ? "Except" : "Intersect")}({enumerableSecondValueAsString}).Any()"); - } - } - } - } - else if (column.FilterPropertyType == typeof(TimeOnly) || column.FilterPropertyType == typeof(TimeOnly?)) - { - value = v != null ? ((TimeOnly)v).ToString("HH:mm:ss") : ""; - secondValue = sv != null ? ((TimeOnly)sv).ToString("HH:mm:ss") : ""; - } - else if (column.FilterPropertyType == typeof(Guid) || column.FilterPropertyType == typeof(Guid?)) - { - value = $"{v}"; - secondValue = $"{sv}"; - } - else if (PropertyAccess.IsDate(column.FilterPropertyType)) - { - if (v != null) - { - - value = - v is DateTime ? ((DateTime)v).ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture) - : v is DateTimeOffset ? ((DateTimeOffset)v).UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture) - : -#if NET6_0_OR_GREATER - v is DateOnly ? ((DateOnly)v).ToString("yyy-MM-dd", CultureInfo.InvariantCulture) : ""; -#else - ""; -#endif - } - if (sv != null) - { - secondValue = - sv is DateTime ? ((DateTime)sv).ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture) - : sv is DateTimeOffset ? ((DateTimeOffset)sv).UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture) - : -#if NET6_0_OR_GREATER - sv is DateOnly ? ((DateOnly)sv).ToString("yyy-MM-dd", CultureInfo.InvariantCulture) : ""; -#else - ""; -#endif - } - } - else if (!(v != null && IsEnumerable(v.GetType())) && (PropertyAccess.IsEnum(column.FilterPropertyType) || PropertyAccess.IsNullableEnum(column.FilterPropertyType))) - { - Type enumType = Enum.GetUnderlyingType(Nullable.GetUnderlyingType(column.FilterPropertyType) ?? column.FilterPropertyType); - if (v != null) - { - value = Convert.ChangeType(v, enumType).ToString(); - } - if (sv != null) - { - secondValue = Convert.ChangeType(sv, enumType).ToString(); - } - } - else - { - value = (string)Convert.ChangeType(column.GetFilterValue(), typeof(string), CultureInfo.InvariantCulture); - secondValue = (string)Convert.ChangeType(column.GetSecondFilterValue(), typeof(string), CultureInfo.InvariantCulture); + combinedExpression = combinedExpression == null + ? expression + : gridLogicalFilterOperator == LogicalFilterOperator.And ? + Expression.AndAlso(combinedExpression, expression) : + Expression.OrElse(combinedExpression, expression); } - if (!string.IsNullOrEmpty(value) || column.GetFilterOperator() == FilterOperator.IsNotNull - || column.GetFilterOperator() == FilterOperator.IsNull - || column.GetFilterOperator() == FilterOperator.IsEmpty - || column.GetFilterOperator() == FilterOperator.IsNotEmpty) + if (combinedExpression != null) { - var linqOperator = LinqFilterOperators[column.GetFilterOperator()]; - if (linqOperator == null) - { - linqOperator = "=="; - } - - var booleanOperator = column.LogicalFilterOperator == LogicalFilterOperator.And ? "&&" : "||"; - - if (string.IsNullOrEmpty(secondValue)) - { - whereList.Add(GetColumnFilter(column, value)); - } - else - { - whereList.Add($"({GetColumnFilter(column, value)} {booleanOperator} {GetColumnFilter(column, secondValue, true)})"); - } + filterExpression = serializer.Serialize(Expression.Lambda>(combinedExpression, parameter)); } } - return whereList.Any() ? - "it => " + string.Join($" {gridBooleanOperator} ", whereList.Where(i => !string.IsNullOrEmpty(i))) : ""; } - return ""; + var columnsWithCustomFilter = columns.Where(canFilterCustom); + + var customFilterExpression = ""; + + if (columnsToFilter.Any()) + { + var expressions = columnsWithCustomFilter.Select(c => (c.GetCustomFilterExpression() ?? "").Replace(" or ", " || ").Replace(" and ", " && ")).Where(e => !string.IsNullOrEmpty(e)).ToList(); + customFilterExpression = string.Join($"{(gridLogicalFilterOperator == LogicalFilterOperator.And ? " && " : " || ")}", expressions); + + return !string.IsNullOrEmpty(filterExpression) && !string.IsNullOrEmpty(customFilterExpression) ? + $"{filterExpression} {(gridLogicalFilterOperator == LogicalFilterOperator.And ? " && " : " || ")} {customFilterExpression}" : + !string.IsNullOrEmpty(customFilterExpression) ? "it => " + customFilterExpression : filterExpression; + } + + return filterExpression; } /// @@ -764,377 +610,38 @@ namespace Radzen public static string ToFilterString(this RadzenDataFilter dataFilter) { Func canFilter = (c) => dataFilter.properties.Where(col => col.Property == c.Property).FirstOrDefault()?.FilterPropertyType != null && - (!(c.FilterValue == null || c.FilterValue as string == string.Empty) - || c.FilterOperator == FilterOperator.IsNotNull || c.FilterOperator == FilterOperator.IsNull - || c.FilterOperator == FilterOperator.IsEmpty || c.FilterOperator == FilterOperator.IsNotEmpty) - && c.Property != null; + (!(c.FilterValue == null || c.FilterValue as string == string.Empty) + || c.FilterOperator == FilterOperator.IsNotNull || c.FilterOperator == FilterOperator.IsNull + || c.FilterOperator == FilterOperator.IsEmpty || c.FilterOperator == FilterOperator.IsNotEmpty) + && c.Property != null; if (dataFilter.Filters.Concat(dataFilter.Filters.SelectManyRecursive(i => i.Filters ?? Enumerable.Empty())).Where(canFilter).Any()) { - return CompositeFilterToFilterString(dataFilter.Filters, dataFilter, dataFilter.LogicalFilterOperator); - } - return ""; - } + var serializer = new ExpressionSerializer(); - /// - /// Creates a Linq-compatible filter string for a list of CompositeFilterDescriptions - /// - /// The type that is being filtered - /// The list if filters - /// The RadzenDataFilter component - /// Whether filter elements should be and-ed or or-ed - /// - private static string CompositeFilterToFilterString(IEnumerable filters, RadzenDataFilter Datafilter, LogicalFilterOperator filterOperator) - { - if (filters.Any()) - { - var LogicalFilterOperator = filterOperator; - var BooleanOperator = LogicalFilterOperator == LogicalFilterOperator.And ? "&&" : "||"; + var filterExpressions = new List(); - var whereList = new List(); - foreach (var column in filters) + var parameter = Expression.Parameter(typeof(T), "x"); + + foreach (var filter in dataFilter.Filters) { - if (column.Filters is not null && column.Filters.Any()) - { - whereList.Add($"({CompositeFilterToFilterString(column.Filters, Datafilter, column.LogicalFilterOperator)})"); - } - if (column.Property is not null) - { - whereList.Add($"{GetColumnFilter(Datafilter, column, Datafilter.FilterCaseSensitivity == FilterCaseSensitivity.CaseInsensitive)}"); - } + AddWhereExpression(parameter, filter, ref filterExpressions, dataFilter.FilterCaseSensitivity); } - return string.Join($" {BooleanOperator} ", whereList.Where(i => !string.IsNullOrEmpty(i))); - } + Expression combinedExpression = null; - return ""; + foreach (var expression in filterExpressions) + { + combinedExpression = combinedExpression == null + ? expression + : dataFilter.LogicalFilterOperator == LogicalFilterOperator.And ? + Expression.AndAlso(combinedExpression, expression) : + Expression.OrElse(combinedExpression, expression); + } - } - - /// - /// Create a single linq-compatible filter string for one datafilter element (includes sub-lists) - /// - /// The type that is being filtered - /// The RadzenDataFilter component - /// The filter elements for which to create the filter string - /// Whether filtering is case sensitive or not - /// - private static string GetColumnFilter(RadzenDataFilter dataFilter, CompositeFilterDescriptor column, bool caseSensitive) - { - var property = column.Property; - - if (property.Contains(".", StringComparison.CurrentCulture)) - { - property = $"({property})"; - } - - var columnInfo = dataFilter.properties.Where(c => c.Property == column.Property).FirstOrDefault(); - if (columnInfo == null) return ""; - - var columnType = columnInfo.FilterPropertyType; - - var columnFilterOperator = column.FilterOperator; - - var linqOperator = LinqFilterOperators[columnFilterOperator.Value]; - linqOperator ??= "=="; - - var value = ""; - - if (columnType == typeof(string)) - { - value = (string)Convert.ChangeType(column.FilterValue, typeof(string)); - value = value?.Replace("\"", "\\\""); - - string filterCaseSensitivityOperator = caseSensitive ? ".ToLower()" : ""; - - if (!string.IsNullOrEmpty(value) && column.FilterOperator == FilterOperator.Contains) + if (combinedExpression != null) { - return $@"({property} == null ? """" : {property}){filterCaseSensitivityOperator}.Contains(""{value}""{filterCaseSensitivityOperator})"; - } - else if (!string.IsNullOrEmpty(value) && columnFilterOperator == FilterOperator.DoesNotContain) - { - return $@"!({property} == null ? """" : {property}){filterCaseSensitivityOperator}.Contains(""{value}""{filterCaseSensitivityOperator})"; - } - else if (!string.IsNullOrEmpty(value) && columnFilterOperator == FilterOperator.StartsWith) - { - return $@"({property} == null ? """" : {property}){filterCaseSensitivityOperator}.StartsWith(""{value}""{filterCaseSensitivityOperator})"; - } - else if (!string.IsNullOrEmpty(value) && columnFilterOperator == FilterOperator.EndsWith) - { - return $@"({property} == null ? """" : {property}){filterCaseSensitivityOperator}.EndsWith(""{value}""{filterCaseSensitivityOperator})"; - } - else if (!string.IsNullOrEmpty(value) && columnFilterOperator == FilterOperator.Equals) - { - return $@"({property} == null ? """" : {property}){filterCaseSensitivityOperator} == ""{value}""{filterCaseSensitivityOperator}"; - } - else if (!string.IsNullOrEmpty(value) && columnFilterOperator == FilterOperator.NotEquals) - { - return $@"({property} == null ? """" : {property}){filterCaseSensitivityOperator} != ""{value}""{filterCaseSensitivityOperator}"; - } - else if (columnFilterOperator == FilterOperator.IsNull) - { - return property + " == null"; - } - else if (columnFilterOperator == FilterOperator.IsEmpty) - { - return property + @" == """""; - } - else if (columnFilterOperator == FilterOperator.IsNotEmpty) - { - return property + @" != """""; - } - else if (columnFilterOperator == FilterOperator.IsNotNull) - { - return property + @" != null"; - } - } - else if (PropertyAccess.IsNumeric(columnType)) - { - value = (string)Convert.ChangeType(column.FilterValue, typeof(string)); - - if (columnFilterOperator == FilterOperator.IsNull || columnFilterOperator == FilterOperator.IsNotNull) - { - return $"{property} {linqOperator} null"; - } - else if (columnFilterOperator == FilterOperator.IsEmpty || columnFilterOperator == FilterOperator.IsNotEmpty) - { - return $@"{property} {linqOperator} """""; - } - else - { - return $"{property} {linqOperator} {value}"; - } - } - else if (columnType == typeof(bool) || columnType == typeof(bool?)) - { - value = (string)Convert.ChangeType(column.FilterValue, typeof(string)); - - return $"{property} {linqOperator} {(columnFilterOperator == FilterOperator.IsNull || columnFilterOperator == FilterOperator.IsNotNull ? "null" : value)}"; - } - else if (PropertyAccess.IsDate(columnType)) - { - var v = column.FilterValue; - if (v != null) - { - value = $@"DateTime(""{(v is DateTime time ? time.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture) : v is DateTimeOffset offset ? offset.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture) : "")}"")"; - } - } - else if (PropertyAccess.IsEnum(columnType) || PropertyAccess.IsNullableEnum(columnType)) - { - var v = column.FilterValue; - if (v != null) - { - value = ((int)v).ToString(); - } - } - else if (IsEnumerable(columnType) && columnType != typeof(string)) - { - var v = column.FilterValue; - var enumerableValue = ((IEnumerable)(v ?? Enumerable.Empty())).AsQueryable(); - - var enumerableValueAsString = "(" + String.Join(",", - (enumerableValue.ElementType == typeof(string) ? - enumerableValue.Cast().Select(i => $@"""{i}""").Cast() - : PropertyAccess.IsDate(enumerableValue.ElementType) ? - enumerableValue.Cast().Select(i => $@"DateTime(""{i}"")").Cast() - : enumerableValue.Cast())) + ")"; - - - if (enumerableValue?.Cast().Any() == true) - { - if (property.Contains(".")) - { - property = $"({property})"; - } - - if (columnFilterOperator == FilterOperator.Contains || columnFilterOperator == FilterOperator.DoesNotContain) - { - return $@"{property} {(columnFilterOperator == FilterOperator.DoesNotContain ? "not " : "in")} {enumerableValueAsString}"; - } - else if (columnFilterOperator == FilterOperator.In || columnFilterOperator == FilterOperator.NotIn) - { - return $@"({property}).{(columnFilterOperator == FilterOperator.NotIn ? "Except" : "Intersect")}({enumerableValueAsString}).Any()"; - } - } - } - else - { - value = (string)Convert.ChangeType(column.FilterValue, typeof(string), CultureInfo.InvariantCulture); - } - - if (!string.IsNullOrEmpty(value) || column.FilterOperator == FilterOperator.IsNotNull - || column.FilterOperator == FilterOperator.IsNull - || column.FilterOperator == FilterOperator.IsEmpty - || column.FilterOperator == FilterOperator.IsNotEmpty) - { - return $"({property} {linqOperator} {(columnFilterOperator == FilterOperator.IsNull || columnFilterOperator == FilterOperator.IsNotNull ? "null" : value)})"; - } - - return ""; - } - - /// - /// Gets the column filter. - /// - /// - /// The column. - /// The value. - /// if set to true [second]. - /// System.String. - private static string GetColumnFilter(RadzenDataGridColumn column, string value, bool second = false) - { - 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; - - var columnFilterOperator = !second ? column.GetFilterOperator() : column.GetSecondFilterOperator(); - - if (columnFilterOperator == FilterOperator.Custom) - { - return ""; - } - - var linqOperator = LinqFilterOperators[columnFilterOperator]; - if (linqOperator == null) - { - linqOperator = "=="; - } - bool isDateOnly = false; - -#if NET6_0_OR_GREATER - if (column.FilterPropertyType == typeof(DateOnly) || column.FilterPropertyType == typeof(DateOnly?)) - { - isDateOnly = true; - } -#endif - - if (column.FilterPropertyType == typeof(string)) - { - string filterCaseSensitivityOperator = column.Grid.FilterCaseSensitivity == FilterCaseSensitivity.CaseInsensitive ? ".ToLower()" : ""; - value = value?.Replace("\"", "\\\""); - - if (!string.IsNullOrEmpty(value) && columnFilterOperator == FilterOperator.Contains) - { - return $@"({property} == null ? """" : {property}){filterCaseSensitivityOperator}.Contains(""{value}""{filterCaseSensitivityOperator})"; - } - else if (!string.IsNullOrEmpty(value) && columnFilterOperator == FilterOperator.DoesNotContain) - { - return $@"!({property} == null ? """" : {property}){filterCaseSensitivityOperator}.Contains(""{value}""{filterCaseSensitivityOperator})"; - } - else if (!string.IsNullOrEmpty(value) && columnFilterOperator == FilterOperator.StartsWith) - { - return $@"({property} == null ? """" : {property}){filterCaseSensitivityOperator}.StartsWith(""{value}""{filterCaseSensitivityOperator})"; - } - else if (!string.IsNullOrEmpty(value) && columnFilterOperator == FilterOperator.EndsWith) - { - return $@"({property} == null ? """" : {property}){filterCaseSensitivityOperator}.EndsWith(""{value}""{filterCaseSensitivityOperator})"; - } - else if (!string.IsNullOrEmpty(value) && columnFilterOperator == FilterOperator.Equals) - { - return $@"({property} == null ? """" : {property}){filterCaseSensitivityOperator} == ""{value}""{filterCaseSensitivityOperator}"; - } - else if (!string.IsNullOrEmpty(value) && columnFilterOperator == FilterOperator.NotEquals) - { - return $@"({property} == null ? """" : {property}){filterCaseSensitivityOperator} != ""{value}""{filterCaseSensitivityOperator}"; - } - else if (columnFilterOperator == FilterOperator.IsNull) - { - return npProperty + " == null"; - } - else if (columnFilterOperator == FilterOperator.IsEmpty) - { - return npProperty + @" == """""; - } - else if (columnFilterOperator == FilterOperator.IsNotEmpty) - { - return npProperty + @" != """""; - } - else if (columnFilterOperator == FilterOperator.IsNotNull) - { - return npProperty + @" != null"; - } - } - else if (PropertyAccess.IsEnum(column.FilterPropertyType) || PropertyAccess.IsNullableEnum(column.FilterPropertyType)) - { - if (columnFilterOperator == FilterOperator.IsNull || columnFilterOperator == FilterOperator.IsNotNull) - { - return $"{property} {linqOperator} null"; - } - else - { - return $"{property} {linqOperator} {value}"; - } - } - else if (PropertyAccess.IsNumeric(column.FilterPropertyType)) - { - if (columnFilterOperator == FilterOperator.IsNull || columnFilterOperator == FilterOperator.IsNotNull) - { - return $"{property} {linqOperator} null"; - } - else if (columnFilterOperator == FilterOperator.IsEmpty || columnFilterOperator == FilterOperator.IsNotEmpty) - { - return $@"{property} {linqOperator} """""; - } - else - { - return $"{property} {linqOperator} {value}"; - } - } - else if (column.FilterPropertyType == typeof(DateTime) || - column.FilterPropertyType == typeof(DateTime?) || - column.FilterPropertyType == typeof(DateTimeOffset) || - column.FilterPropertyType == typeof(DateTimeOffset?) || isDateOnly) - { - if (columnFilterOperator == FilterOperator.IsNull || columnFilterOperator == FilterOperator.IsNotNull) - { - return $"{property} {linqOperator} null"; - } - else if (columnFilterOperator == FilterOperator.IsEmpty || columnFilterOperator == FilterOperator.IsNotEmpty) - { - return $@"{property} {linqOperator} """""; - } - else - { - var dateTimeValue = DateTime.Parse(value, CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.RoundtripKind); - var finalDate = dateTimeValue.TimeOfDay == TimeSpan.Zero ? dateTimeValue.Date : dateTimeValue; - var dateFormat = dateTimeValue.TimeOfDay == TimeSpan.Zero ? "yyyy-MM-dd" : "yyyy-MM-ddTHH:mm:ss.fffZ"; - - string dateFunction = "DateTime"; //fallback to datetime, if it's an offset or dateonly use that - if (column.FilterPropertyType == typeof(DateTimeOffset) || column.FilterPropertyType == typeof(DateTimeOffset?)) - dateFunction = "DateTimeOffset"; - else - { -#if NET6_0_OR_GREATER - if (column.FilterPropertyType == typeof(DateOnly) || column.FilterPropertyType == typeof(DateOnly?)) - dateFunction = "DateOnly"; -#endif - } - - - return $@"{property} {linqOperator} {dateFunction}(""{finalDate.ToString(dateFormat, CultureInfo.InvariantCulture)}"")"; - } - } - else if (column.FilterPropertyType == typeof(bool) || column.FilterPropertyType == typeof(bool?)) - { - value = $"{value}".ToLower(); - return $"{property} {linqOperator} {(columnFilterOperator == FilterOperator.IsNull || columnFilterOperator == FilterOperator.IsNotNull ? "null" : value)}"; - } - else if (column.FilterPropertyType == typeof(Guid) || column.FilterPropertyType == typeof(Guid?)) - { - if (columnFilterOperator == FilterOperator.IsNull || columnFilterOperator == FilterOperator.IsNotNull) - { - return $"{property} {linqOperator} null"; - } - else if (columnFilterOperator == FilterOperator.IsEmpty || columnFilterOperator == FilterOperator.IsNotEmpty) - { - return $@"{property} {linqOperator} """""; - } - else - { - return $@"{property} {linqOperator} Guid(""{value}"")"; + return serializer.Serialize(Expression.Lambda>(combinedExpression, parameter)); } } @@ -1159,7 +666,7 @@ namespace Radzen ? null : (string)Convert.ChangeType(filterValue is DateTimeOffset ? ((DateTimeOffset)filterValue).UtcDateTime : filterValue is DateOnly ? - ((DateOnly)filterValue).ToString("yyy-MM-dd", CultureInfo.InvariantCulture) : + ((DateOnly)filterValue).ToString("yyy-MM-dd", CultureInfo.InvariantCulture) : filterValue, typeof(string), CultureInfo.InvariantCulture); if (column.Grid.FilterCaseSensitivity == FilterCaseSensitivity.CaseInsensitive && column.FilterPropertyType == typeof(string)) @@ -1276,7 +783,7 @@ namespace Radzen column.FilterPropertyType == typeof(DateTime?) || column.FilterPropertyType == typeof(DateTimeOffset) || column.FilterPropertyType == typeof(DateTimeOffset?) || - column.FilterPropertyType == typeof(DateOnly) || + column.FilterPropertyType == typeof(DateOnly) || column.FilterPropertyType == typeof(DateOnly?)) { if (columnFilterOperator == FilterOperator.IsNull || columnFilterOperator == FilterOperator.IsNotNull) @@ -1394,16 +901,16 @@ namespace Radzen if (columnsToFilter.Any()) { source = source.Where(columnsToFilter.Select(c => new FilterDescriptor() - { - Property = c.Property, - FilterProperty = c.FilterProperty, - Type = c.FilterPropertyType, - FilterValue = c.GetFilterValue(), - FilterOperator = c.GetFilterOperator(), - SecondFilterValue = c.GetSecondFilterValue(), - SecondFilterOperator = c.GetSecondFilterOperator(), - LogicalFilterOperator = c.GetLogicalFilterOperator() - }), gridLogicalFilterOperator, gridFilterCaseSensitivity); + { + Property = c.Property, + FilterProperty = c.FilterProperty, + Type = c.FilterPropertyType, + FilterValue = c.GetFilterValue(), + FilterOperator = c.GetFilterOperator(), + SecondFilterValue = c.GetSecondFilterValue(), + SecondFilterOperator = c.GetSecondFilterOperator(), + LogicalFilterOperator = c.GetLogicalFilterOperator() + }), gridLogicalFilterOperator, gridFilterCaseSensitivity); } var columnsWithCustomFilter = columns.Where(canFilterCustom); @@ -1411,7 +918,7 @@ namespace Radzen if (columnsToFilter.Any()) { var expressions = columnsWithCustomFilter.Select(c => (c.GetCustomFilterExpression() ?? "").Replace(" or ", " || ").Replace(" and ", " && ")).Where(e => !string.IsNullOrEmpty(e)).ToList(); - source = expressions.Any() ? + source = expressions.Any() ? System.Linq.Dynamic.Core.DynamicExtensions.Where(source, "it => " + string.Join($"{(gridLogicalFilterOperator == LogicalFilterOperator.And ? " && " : " || ")}", expressions)) : source; } @@ -1472,7 +979,7 @@ namespace Radzen /// IQueryable<T>. public static IQueryable Where(this IQueryable source, IEnumerable filters, LogicalFilterOperator logicalFilterOperator, FilterCaseSensitivity filterCaseSensitivity) { - Func canFilter = (c) => + Func canFilter = (c) => (!(c.FilterValue == null || c.FilterValue as string == string.Empty) || c.FilterOperator == FilterOperator.IsNotNull || c.FilterOperator == FilterOperator.IsNull || c.FilterOperator == FilterOperator.IsEmpty || c.FilterOperator == FilterOperator.IsNotEmpty) @@ -1820,4 +1327,4 @@ namespace Radzen .ToList(); } } -} +} \ No newline at end of file diff --git a/Radzen.Blazor/Radzen.Blazor.csproj b/Radzen.Blazor/Radzen.Blazor.csproj index 4ddf42503..19c91a157 100644 --- a/Radzen.Blazor/Radzen.Blazor.csproj +++ b/Radzen.Blazor/Radzen.Blazor.csproj @@ -25,7 +25,6 @@ - @@ -56,10 +55,10 @@ - + - + <_SassFileList>@(Sass->'"%(FullPath)"', ' ') files $(_SassFileList) --outputstyle $(DartSassOutputStyle) --level $(DartSassOutputLevel) @@ -68,14 +67,14 @@ - + - + diff --git a/Radzen.Blazor/RadzenAccordion.razor b/Radzen.Blazor/RadzenAccordion.razor index 661d54995..3d0353df5 100644 --- a/Radzen.Blazor/RadzenAccordion.razor +++ b/Radzen.Blazor/RadzenAccordion.razor @@ -1,4 +1,5 @@ @using System.Linq +@using Radzen.Blazor.Rendering @inherits RadzenComponent @@ -21,14 +22,7 @@ - @if (IsSelected(i, item)) - { -
-
- @item.ChildContent -
+ +
+ @item.ChildContent
- } +
}
} \ No newline at end of file diff --git a/Radzen.Blazor/RadzenAccordion.razor.cs b/Radzen.Blazor/RadzenAccordion.razor.cs index 61c9654ae..c3a529e7f 100644 --- a/Radzen.Blazor/RadzenAccordion.razor.cs +++ b/Radzen.Blazor/RadzenAccordion.razor.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; +using Radzen.Blazor.Rendering; using System; using System.Collections.Generic; using System.Linq; @@ -110,6 +111,11 @@ namespace Radzen.Blazor } } + string ToggleIconClass(RadzenAccordionItem item) => ClassList.Create("notranslate rz-accordion-toggle-icon rzi") + .Add("rz-state-expanded", item.GetSelected()) + .Add("rz-state-collapsed", !item.GetSelected()) + .ToString(); + /// /// Refreshes this instance. /// diff --git a/Radzen.Blazor/RadzenAlert.razor.cs b/Radzen.Blazor/RadzenAlert.razor.cs index 821e88b81..78fa275be 100644 --- a/Radzen.Blazor/RadzenAlert.razor.cs +++ b/Radzen.Blazor/RadzenAlert.razor.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; +using Radzen.Blazor.Rendering; using System; using System.Threading.Tasks; @@ -161,32 +162,23 @@ namespace Radzen.Blazor } /// - protected override string GetComponentCssClass() - { - return $"rz-alert rz-alert-{GetAlertSize()} rz-variant-{Enum.GetName(typeof(Variant), Variant).ToLowerInvariant()} rz-{Enum.GetName(typeof(AlertStyle), AlertStyle).ToLowerInvariant()} rz-shade-{Enum.GetName(typeof(Shade), Shade).ToLowerInvariant()}"; - } + protected override string GetComponentCssClass() => ClassList.Create("rz-alert") + .Add($"rz-alert-{GetAlertSize()}") + .AddVariant(Variant) + .Add($"rz-{Enum.GetName(typeof(AlertStyle), AlertStyle).ToLowerInvariant()}") + .AddShade(Shade) + .ToString(); - string GetIcon() - { - if (!string.IsNullOrEmpty(Icon)) - { - return Icon; - } - - switch (AlertStyle) - { - case AlertStyle.Success: - return "check_circle"; - case AlertStyle.Danger: - return "error"; - case AlertStyle.Warning: - return "warning_amber"; - case AlertStyle.Info: - return "info"; - default: - return "lightbulb"; - } - } + string GetIcon() => !string.IsNullOrEmpty(Icon) + ? Icon + : AlertStyle switch + { + AlertStyle.Success => "check_circle", + AlertStyle.Danger => "error", + AlertStyle.Warning => "warning_amber", + AlertStyle.Info => "info", + _ => "lightbulb", + }; async Task OnClose() { @@ -221,4 +213,4 @@ namespace Radzen.Blazor } } } -} +} \ No newline at end of file diff --git a/Radzen.Blazor/RadzenAutoComplete.razor b/Radzen.Blazor/RadzenAutoComplete.razor index 12cfb9abd..c0dc3eea0 100644 --- a/Radzen.Blazor/RadzenAutoComplete.razor +++ b/Radzen.Blazor/RadzenAutoComplete.razor @@ -14,7 +14,7 @@