Skip to content

MudForm: Support For expressions refering to non-nullable properties#9180

Merged
henon merged 5 commits intoMudBlazor:v6from
lieszkol:v6.20.0-local
Jun 24, 2024
Merged

MudForm: Support For expressions refering to non-nullable properties#9180
henon merged 5 commits intoMudBlazor:v6from
lieszkol:v6.20.0-local

Conversation

@lieszkol
Copy link

Description

If a For validation property refers to a non-nullable field, then currently the framework throws errors such as "'Unable to cast object of type 'System.Linq.Expressions.UnaryExpression' to type 'System.Linq.Expressions.MemberExpression'."

This PR fixes this by properly handling validation Expressions that refer to non-nullable properties, more specifically, where For.Body is not a MemberExpression but a UnaryExpression and For.Body.Operand is the MemberExpression.

Resolves #8931

How Has This Been Tested?

visually

Type of Changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation (fix or improvement to the website or code docs)

Checklist

  • The PR is submitted to the correct branch (dev).
  • My code follows the code style of this project.
  • I've added relevant tests.

Submitting to branch v6 since I am working on a live project and v7 is not ready for production. I can create a new PR for dev if this PR is accepted.

@github-actions github-actions bot added bug Unexpected behavior or functionality not working as intended PR: needs review labels Jun 14, 2024
@lieszkol lieszkol changed the title Add handling for validation expressions that refer to non-nullable properties (#8931) Add handling for validation expressions that refer to non-nullable properties Jun 14, 2024
@codecov
Copy link

codecov bot commented Jun 14, 2024

Codecov Report

Attention: Patch coverage is 81.25000% with 3 lines in your changes missing coverage. Please review.

Project coverage is 88.79%. Comparing base (bf2e9ef) to head (14c6a57).
Report is 3 commits behind head on v6.

Files Patch % Lines
src/MudBlazor/Base/MudFormComponent.cs 78.57% 1 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##               v6    #9180      +/-   ##
==========================================
- Coverage   88.82%   88.79%   -0.03%     
==========================================
  Files         416      394      -22     
  Lines       12356    12302      -54     
  Branches     2458     2449       -9     
==========================================
- Hits        10975    10924      -51     
+ Misses        850      847       -3     
  Partials      531      531              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@lieszkol
Copy link
Author

Sorry, I did not mean to push the second set of changes with commit message "Add handling for validation expressions that refer to non-nullable properties" onto this PR, I've reverted that commit.

@Mr-Technician
Copy link
Member

@henon should this include a couple of test cases?

@henon
Copy link
Contributor

henon commented Jun 18, 2024

Yes please add a test that fails without the fix and works with it.

@lieszkol
Copy link
Author

I've found one more spot where this issue arises (ExpressionExtensions.GetLabelString), added a fix as well as unit tests for all changes that fail without them and suceed with them.

Let me expand a little bit more on the reason for this PR now that I have a better understanding of the code. Some components, such as MudDatePicker, only allow binding to a DateTime? field. However, if the corressponding database field is marked as NOT NULL, tools such as EF Core Power Tools generate a non-nullable backing field public DateTime DeliveryDay { get; set; }. To work around this, it is possible to create a component that includes a MudDatePicker and uses an intermediate field to enable the required functionality. For example, "ZenDatePicker.cs":

<MudDatePicker @bind-Date="InternalDate" For="@For" Required="true" />

@code {
	DateTime? InternalDate
	{
		get => new DateTime?(DateRequired);
		set
		{
			if (!EqualityComparer<DateTime>.Default.Equals(value ?? default, DateRequired))
			{
				DateRequired = value ?? DefaultValue ?? default;
				DateRequiredChanged.InvokeAsync(DateRequired);
			}
		}
	}

	[Parameter]
	public DateTime DateRequired { get; set; }

	[Parameter] public EventCallback<DateTime> DateRequiredChanged { get; set; }

	[Parameter]
	public DateTime? DefaultValue { get; set; } = DateTime.Now;

	[Parameter]
	public Expression<Func<DateTime?>>? For { get; set; }
}

This component can then be bound to the non-nullable backing field:
<ZenDatePicker @bind-DateRequired="Entity.DeliveryDay" For="@(() => Entity.DeliveryDay)" />

Note how the type for the For property has to be DateTime? because MudFormComponent<T, U> uses the same type parameter T both for the For property as well as the value property:

public Expression<Func<T>>? For { get; set; }
protected T? _value;

and MudDatePicker inherits from MudPicker with T=DateTime?: MudBaseDatePicker : MudPicker<DateTime?>

So basically another solution to this problem could be introducing a separate type parameter into MudFormComponent to enable inheriting classes to set differing types for the value and the for fields: MudFormComponent<TValue, TFor, U> and then also adjusting components that inherit from MudFormComponent. This could even be useful in situations where, say, one wants to make use of validators defined on a model for one non-nullable field, but bind the control to a different (nullable) field. But this would be a lot more work so it's easiest to just make sure that the framework can deal with situations where For is a Expression<Func<DateTime?>> that refers to a non-nullable field.

@henon
Copy link
Contributor

henon commented Jun 19, 2024

But this would be a lot more work so it's easiest to just make sure that the framework can deal with situations where For is a Expression<Func<DateTime?>> that refers to a non-nullable field.

Yes I agree. There is always the option of composition (wrapping) vs inheriting which should also solve the problem.

@henon
Copy link
Contributor

henon commented Jun 19, 2024

Just noticed this is a PR against v6. That makes it even more reasonable not to do any complicated PRs.

@henon henon added the v6 label Jun 19, 2024
@henon
Copy link
Contributor

henon commented Jun 19, 2024

To get the code quality fixed please execute dotnet format on the VS developer console.

@ScarletKuro
Copy link
Member

To get the code quality fixed please execute dotnet format on the VS developer console.

I think It would be better to do as separate PR and update this one, otherwise too much unrelated changes will be in the PR. I will do that.

Just noticed this is a PR against v6

Yes, I think we should leave it for v6 only for now. Since we plan to rework the validation anyway, we also have this #9016

@ScarletKuro
Copy link
Member

I will do that.

One PR down #9216
But there is also need to port this change #8518

@ScarletKuro
Copy link
Member

There is always the option of composition (wrapping) vs inheriting which should also solve the problem.

Wrapping is better in case of Blazor, otherwise you will hit this problem #9052 (comment)

"Trimming",
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
Justification = "Application code does not get trimmed. We expect the members in the expression to not be trimmed.")]
static Func<object, object> CreateAccessor((Type model, string member) arg)
Copy link
Member

@ScarletKuro ScarletKuro Jun 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not that big of an expert in Expression area, but would something like this work?

static Func<object, object> CreateAccessor([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields)] Type modelType, string memberName)
{
	var parameter = Expression.Parameter(typeof(object), "value");

	var memberInfo = modelType.GetProperty(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
							?? (MemberInfo?)modelType.GetField(memberName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

	if (memberInfo == null)
	{
		throw new ArgumentException($"No property or field named '{memberName}' found on type '{modelType.FullName}'.");
	}
	Expression expression = Expression.Convert(parameter, modelType);

	if (memberInfo is PropertyInfo propertyInfo)
	{
		expression = Expression.Property(expression, propertyInfo);
	}
	else if (memberInfo is FieldInfo fieldInfo)
	{
		expression = Expression.Field(expression, fieldInfo);
	}

	expression = Expression.Convert(expression, typeof(object));
	var lambda = Expression.Lambda<Func<object, object>>(expression, parameter);

	var func = lambda.Compile();
	return func;
}

To get rid of UnconditionalSuppressMessage?
I think the Expression.PropertyOrField is doing more or less the same, but it's using the unsafe version of GetProperty and GetField that doesn't pass the Type information as it just doesn't have it in it's definition.

@lieszkol
Copy link
Author

There is always the option of composition (wrapping) vs inheriting which should also solve the problem.

Wrapping is better in case of Blazor, otherwise you will hit this problem #9052 (comment)

Not sure where this thread came from, I am using wrapping not inheritance as you can see the code in the "Describe alternatives you've considered" section of #8931.

The CreateAccessor local func comes from the main branch of the aspnetcore repo (defined inside GetModelFromMemberAccess) here: https://github.com/dotnet/aspnetcore/blob/main/src/Components/Forms/src/FieldIdentifier.cs

I should have noted this in the comments.

@ScarletKuro
Copy link
Member

The CreateAccessor local func comes from the main branch of the aspnetcore repo (defined inside GetModelFromMemberAccess) here: https://github.com/dotnet/aspnetcore/blob/main/src/Components/Forms/src/FieldIdentifier.cs

The question wasn't really about its origin, but rather whether my suggested alternative would work, as it's "safer". However, I'm not going to insist on it since the risk of the application code being trimmed is low.

@henon, this is ready for merge.

@ScarletKuro ScarletKuro requested a review from henon June 20, 2024 21:37
@henon henon changed the title Add handling for validation expressions that refer to non-nullable properties MudForm: Support For expressions refering to non-nullable properties Jun 24, 2024
@henon henon merged commit ad2f140 into MudBlazor:v6 Jun 24, 2024
@henon
Copy link
Contributor

henon commented Jun 24, 2024

Thanks everyone

@ScarletKuro
Copy link
Member

Made v6.21.0 release with this change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Unexpected behavior or functionality not working as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants