Skip to content

Instantly share code, notes, and snippets.

@VisualBean
Created September 16, 2025 21:03
Show Gist options
  • Select an option

  • Save VisualBean/64886cc99e5cd61c22ef69fd126a2ffb to your computer and use it in GitHub Desktop.

Select an option

Save VisualBean/64886cc99e5cd61c22ef69fd126a2ffb to your computer and use it in GitHub Desktop.
An optimized parser for encapsulating minimal api queryparams in objects.
public class MyQueryParams : QueryParameterBase<MyQueryParams>
{
public string First { get; set; }
public string Second { get; set; }
}
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