Created
September 16, 2025 21:03
-
-
Save VisualBean/64886cc99e5cd61c22ef69fd126a2ffb to your computer and use it in GitHub Desktop.
An optimized parser for encapsulating minimal api queryparams in objects.
This file contains hidden or 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
| public class MyQueryParams : QueryParameterBase<MyQueryParams> | |
| { | |
| public string First { get; set; } | |
| public string Second { get; set; } | |
| } |
This file contains hidden or 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.Collections.Concurrent; | |
| using System.ComponentModel; | |
| using System.Diagnostics.CodeAnalysis; | |
| using System.Globalization; | |
| using System.Linq.Expressions; | |
| using System.Reflection; | |
| using System.Web; | |
| public static class QueryParameterParser | |
| { | |
| private static readonly ConcurrentDictionary<Type, PropertyCache[]> _propertyCache = new(); | |
| public static bool TryParse<T>([NotNullWhen(true)] string? s, IFormatProvider? provider, | |
| [MaybeNullWhen(false)] out T result) where T : new() | |
| { | |
| result = default; | |
| if (string.IsNullOrWhiteSpace(s)) | |
| { | |
| result = new T(); | |
| return true; | |
| } | |
| try | |
| { | |
| result = new T(); | |
| var queryParams = HttpUtility.ParseQueryString(s); | |
| var properties = GetCachedProperties<T>(); | |
| foreach (var prop in properties) | |
| { | |
| var paramValue = queryParams[prop.Name]; | |
| if (paramValue != null) | |
| { | |
| if (TryConvertValue(paramValue, prop.PropertyType, provider, out var convertedValue)) | |
| { | |
| prop.Setter(result, convertedValue); | |
| } | |
| } | |
| } | |
| return true; | |
| } | |
| catch | |
| { | |
| result = default; | |
| return false; | |
| } | |
| } | |
| private static PropertyCache[] GetCachedProperties<T>() | |
| { | |
| return _propertyCache.GetOrAdd(typeof(T), type => | |
| { | |
| return type.GetProperties(BindingFlags.Public | BindingFlags.Instance) | |
| .Where(p => p.CanWrite) | |
| .Select(p => new PropertyCache( | |
| p.Name, | |
| p.PropertyType, | |
| CreateSetter(p) | |
| )) | |
| .ToArray(); | |
| }); | |
| } | |
| private static Action<object, object?> CreateSetter(PropertyInfo property) | |
| { | |
| var targetParam = Expression.Parameter(typeof(object), "target"); | |
| var valueParam = Expression.Parameter(typeof(object), "value"); | |
| var convertedTarget = Expression.Convert(targetParam, property.DeclaringType!); | |
| var convertedValue = Expression.Convert(valueParam, property.PropertyType); | |
| var propertyAccess = Expression.Property(convertedTarget, property); | |
| var assignment = Expression.Assign(propertyAccess, convertedValue); | |
| var lambda = Expression.Lambda<Action<object, object?>>( | |
| assignment, targetParam, valueParam); | |
| return lambda.Compile(); | |
| } | |
| public static void ClearCache() | |
| { | |
| _propertyCache.Clear(); | |
| } | |
| public static PropertyCache[] GetTypePropertyCache<T>() | |
| { | |
| return GetCachedProperties<T>(); | |
| } | |
| private static bool TryConvertValue(string value, Type targetType, IFormatProvider? provider, out object? result) | |
| { | |
| result = null; | |
| try | |
| { | |
| var underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType; | |
| if (string.IsNullOrEmpty(value) && targetType != typeof(string)) | |
| { | |
| result = targetType.IsValueType ? Activator.CreateInstance(targetType) : null; | |
| return true; | |
| } | |
| if (underlyingType == typeof(string)) | |
| { | |
| result = value; | |
| return true; | |
| } | |
| var converter = TypeDescriptor.GetConverter(underlyingType); | |
| if (converter.CanConvertFrom(typeof(string))) | |
| { | |
| result = converter.ConvertFromString(null, provider as CultureInfo ?? CultureInfo.InvariantCulture, | |
| value); | |
| return true; | |
| } | |
| result = Convert.ChangeType(value, underlyingType, provider); | |
| return true; | |
| } | |
| catch | |
| { | |
| return false; | |
| } | |
| } | |
| } | |
| public record PropertyCache( | |
| string Name, | |
| Type PropertyType, | |
| Action<object, object?> Setter | |
| ); | |
| public abstract class QueryParameterBase<T> : IParsable<T> where T : QueryParameterBase<T>, new() | |
| { | |
| public static T Parse(string s, IFormatProvider? provider) | |
| { | |
| if (TryParse(s, provider, out var result)) | |
| { | |
| return result; | |
| } | |
| throw new FormatException($"Unable to parse '{s}' as {typeof(T).Name}"); | |
| } | |
| public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, | |
| [MaybeNullWhen(false)] out T result) | |
| { | |
| return QueryParameterParser.TryParse(s, provider, out result); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment