Skip to content

Instantly share code, notes, and snippets.

@DevPoint
Last active February 5, 2025 15:33
Show Gist options
  • Save DevPoint/34e0450c27ae8668dcc7563c61fed256 to your computer and use it in GitHub Desktop.
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.
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