如何无需显式指定TProperty类型创建Expression<Func<TModel, TProperty>>
Expression<Func<TModel, TProperty>> without explicitly specifying TProperty? Great question! The short answer is: you can get close, but there are some tradeoffs since C#'s compile-time type inference can't resolve the property type from a string literal alone. Let's break down how to approach this.
The Core Challenge
When you call CreateExpression<SomeModel>("IsAlive"), the compiler has no way to statically know that "IsAlive" is a bool property on SomeModel—the string is just a runtime value. So we can't get full compile-time type safety for the TProperty parameter without a little extra work. But we have two solid approaches to make this work.
Approach 1: Runtime Reflection with Object Wrapping
We can create a non-generic overload of CreateExpression that uses reflection to find the property's type, then invokes the original generic method under the hood. The result will be wrapped as Expression<Func<TModel, object>>, which works for both value types and reference types (with boxing for value types).
Here's the code implementation:
public static class ExpressionBuilder { // Public non-generic entry point public static Expression<Func<TModel, object>> CreateExpression<TModel>(string propertyName) { var modelType = typeof(TModel); // Find the property or throw if it doesn't exist var propertyInfo = modelType.GetProperty(propertyName) ?? throw new ArgumentException($"Property '{propertyName}' not found on type '{modelType.FullName}'"); // Get a reference to our generic CreateExpression method var genericMethod = typeof(ExpressionBuilder) .GetMethod(nameof(CreateExpressionGeneric), BindingFlags.NonPublic | BindingFlags.Static) ?.MakeGenericMethod(modelType, propertyInfo.PropertyType); if (genericMethod == null) throw new InvalidOperationException("Failed to locate generic expression builder method."); // Invoke the generic method to get the strongly-typed expression var typedExpression = genericMethod.Invoke(null, new object[] { propertyName }) as LambdaExpression; // Wrap value types in a Convert expression to object (handles boxing) if (propertyInfo.PropertyType.IsValueType) { var convertExpr = Expression.Convert(typedExpression.Body, typeof(object)); return Expression.Lambda<Func<TModel, object>>(convertExpr, typedExpression.Parameters); } // For reference types, we can cast directly return typedExpression as Expression<Func<TModel, object>>; } // Internal generic method that builds the strongly-typed expression private static Expression<Func<TModel, TProperty>> CreateExpressionGeneric<TModel, TProperty>(string propertyName) { var parameter = Expression.Parameter(typeof(TModel), "model"); var property = Expression.Property(parameter, propertyName); return Expression.Lambda<Func<TModel, TProperty>>(property, parameter); } }
Usage Example:
// Returns Expression<Func<SomeModel, object>>, with the underlying body targeting a bool property var isAliveExpr = ExpressionBuilder.CreateExpression<SomeModel>("IsAlive"); // Returns Expression<Func<SomeModel, object>> for a string property var stringPropExpr = ExpressionBuilder.CreateExpression<SomeModel>("StringProperty");
If you need the exact strongly-typed expression later, you can cast it (with runtime checking):
var boolExpr = isAliveExpr as Expression<Func<SomeModel, bool>>; if (boolExpr != null) { // Use the strongly-typed expression here }
Approach 2: Source Generators for Compile-Time Safety
If you want full compile-time type safety (so var lambda = CreateExpression<SomeModel>("IsAlive") is actually Expression<Func<SomeModel, bool>> at compile time), you can use a C# Source Generator. This tool runs during compilation, parses the property name string, resolves its type, and generates the strongly-typed code for you.
The basic steps for this approach are:
- Create a source generator that detects calls to your
CreateExpressionmethod. - Use Roslyn's APIs to resolve the
TModeltype and the property name string literal. - Generate code that calls the generic
CreateExpression<TModel, TProperty>with the correctTPropertytype. - The compiler will use this generated code instead, giving you compile-time type checking.
This is more complex but gives you the best of both worlds: concise syntax and type safety.
Key Takeaways
- Without source generators, you can't get compile-time type inference for
TPropertyfrom a string property name—since strings are runtime values. - The reflection approach is simple and works for most runtime scenarios, returning an
Expression<Func<TModel, object>>. - Source generators let you achieve compile-time safety, but require more setup.
内容的提问来源于stack exchange,提问作者Rafael León

