Last active
February 5, 2025 15:33
-
-
Save DevPoint/34e0450c27ae8668dcc7563c61fed256 to your computer and use it in GitHub Desktop.
This method parses property paths like 'SomeProp.MyCollection[123].ChildProp' and returns a FieldIdentifier for use in Blazor ValidationMessageStore.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Reflection; | |
using Microsoft.AspNetCore.Components.Forms; | |
namespace Bookin.Web.Elements | |
{ | |
public static class FieldIdentifierHelper | |
{ | |
private readonly static char[] Separators = ['.', '[']; | |
public static FieldIdentifier FromPropertyPath(object obj, string propertyPath) | |
{ | |
// Most of the credits goes to Steve Sanderson | |
// Github Gist: https://gist.github.com/SteveSandersonMS/090145d7511c5190f62a409752c60d00 | |
// | |
// This method parses property paths like 'SomeProp.MyCollection[123].ChildProp' | |
// and returns a FieldIdentifier which is an (instance, propName) pair. | |
// For example, it would return the pair | |
// (SomeProp.MyCollection[123], "ChildProp"). It traverses | |
// as far into the propertyPath as it can go until it finds any null instance. | |
// | |
// The result could be used to add error messages from an API endpoint or from | |
// FluentValidation to the Blazor ValidationMessageStore | |
// | |
// Modifications based on source code of Blazor.FluentValidation | |
// Github: https://github.com/Blazored/FluentValidation | |
// - Handling cases where no "Item" property exists for indexers. | |
// | |
// Modifications added by Wilfried Reiter, Book-in: | |
// Github Gist: https://gist.github.com/DevPoint/34e0450c27ae8668dcc7563c61fed256 | |
// - BindingFlags are used to allow property access ignoring property names case | |
// to allow to work with error message coming from APIs using camel case naming | |
// conventions | |
// | |
const BindingFlags bindingFlags = BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public; | |
int nextTokenEnd = propertyPath.IndexOfAny(Separators); | |
while (nextTokenEnd >= 0) | |
{ | |
Type type = obj.GetType(); | |
string nextToken = propertyPath[..nextTokenEnd]; | |
if (nextToken.EndsWith(']')) | |
{ | |
// Check if object can be casted to an array | |
string indexerToken = nextToken[..^1]; | |
if (type.IsArray) | |
{ | |
int indexerValue = int.Parse(indexerToken); | |
obj = ((Array)obj).GetValue(indexerValue); | |
} | |
else | |
{ | |
// It's an indexer | |
// This code assumes C# conventions (one indexer named "Item" with one param) | |
PropertyInfo propertyInfo = type.GetProperty("Item"); | |
if (propertyInfo != null) | |
{ | |
Type indexerType = propertyInfo.GetIndexParameters()[0].ParameterType; | |
object indexerValue = Convert.ChangeType(indexerToken, indexerType); | |
obj = propertyInfo.GetValue(obj, [indexerValue]); | |
} | |
// If there is no Item property | |
// Addresses an issue with collection expressions in C# 12 regarding IReadOnlyList: | |
// Generates a <>z__ReadOnlyArray which: | |
// - lacks an Item property, and | |
// - cannot be cast to object[] successfully. | |
// This workaround accesses elements directly using an indexer. | |
// Source: Blazor.FluentValidation/EditContextFluentValidationExtensions.cs | |
else if (obj is IReadOnlyList<object> readOnlyList) | |
{ | |
int indexerValue = int.Parse(indexerToken); | |
obj = readOnlyList[indexerValue]; | |
} | |
else | |
{ | |
throw new InvalidOperationException($"Could not find indexer on object of type {type.FullName}."); | |
} | |
} | |
} | |
else | |
{ | |
// It's an object (class, struct, etc) property | |
PropertyInfo propertyInfo = type.GetProperty(nextToken, bindingFlags); | |
if (propertyInfo == null) | |
{ | |
throw new InvalidOperationException($"Could not find property named '{nextToken}' on object of type {type.FullName}."); | |
} | |
obj = propertyInfo.GetValue(obj); | |
} | |
// next property path element | |
propertyPath = propertyPath[(nextTokenEnd + 1)..]; | |
nextTokenEnd = propertyPath.IndexOfAny(Separators); | |
} | |
// property path last element | |
if (propertyPath.EndsWith(']')) | |
{ | |
// It's an indexer property | |
string indexerToken = propertyPath[..^1]; | |
return new FieldIdentifier(obj, indexerToken); | |
} | |
else | |
{ | |
// It's a common property | |
Type type = obj.GetType(); | |
PropertyInfo propertyInfo = type.GetProperty(propertyPath, bindingFlags); | |
if (propertyInfo == null) | |
{ | |
throw new InvalidOperationException($"Could not find property named '{propertyPath}' on object of type {type.FullName}."); | |
} | |
return new FieldIdentifier(obj, propertyInfo.Name); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment