Linq - How to use Select then Where but Select Entire Entity? - c#

As a stop measure, I'd like developers to get data based on specific criteria but I don't want to expose the entire object for a where clause.
dbContext.BigThing
.Select(s => new LesserThing { FieldA= s.FieldA, FieldB = s.FieldB })
.Where(someQueryExpression)
.TakeTheEntireEntity();
The someQueryExpression will be of type Expression<Func<LesserThing, bool>>.
The TakeTheEntireEntity is sudo code for, how do I get the entire data model? I can use the dbContext again as an inner Where clause but this would trigger 2 queries and evaluate client side, which is bad. One trip to the db is required.
The idea here is to allow developers to consume this service but prevent them from querying Where SomeNonIndexedField cannot be used.

There are many ways to accomplish this (none out-of-the box though), with all they requiring expression tree manipulation.
But if the goal is just to limit the fields available in the Where conditions, there is much simpler approach which works out-of-the-box and requires less coding to apply.
Just make the LesserThing interface and let BiggerThing implement it implicitly. e.g.
public interface ISomeEntityFilter
{
string FieldA { get; }
DateTime FieldB { get; }
}
public class SomeEntity : ISomeEntityFilter
{
public int Id { get; set; }
public string FieldA { get; set; }
public DateTime FieldB { get; set; }
// ... many others
}
Now, given
Expression<Func<ISomeEnityFilter, bool>> filter
coming from the caller, what you do is simply applying it and then casting back to the original type (the latter is needed because the first operation changes the generic type of the result from IQueryable<SomeEntity> to IQueryable<ISomeEntityFilter>, but you know that it sill actually holds SomeEntity elements):
var query = dbContext.Set<SomeEntity>()
.Where(filter)
.Cast<SomeEntity>();
And yes (you can easily verify that), the result is server (SQL) translatable EF Core query.

What you want to do requires working with the Expression tree to transform a lambda of the form pl => pl.prop == x to a new lambda p => p.prop == x.
This code works with either properties or fields, but assumes that the LesserThing will have only member names that also exist in the BigThing. Of course, you could also create a Dictionary and map the LesserThing member names to BigThing member names.
Since you can't infer generic type parameters from the return, you have to manually pass in the types.
var ans = dbContext.BigThing.Where(someQueryExpression.TestBigThing<BigThing,LesserThing>());
Since the type of someQueryExpression must be Expression<Func<LesserThing,bool>> it is only possible to access fields or properties of LesserThing or outside variables or constants.
public static class TestExt {
public static Expression<Func<TBig, bool>> TestBigThing<TBig, TLesser>(this Expression<Func<TLesser, bool>> pred) {
// (T p)
var newParm = Expression.Parameter(typeof(TBig), "p");
var oldMemberExprs = pred.Body.CollectMemberExpressions();
var newMemberExprs = oldMemberExprs.Select(m => Expression.PropertyOrField(newParm, m.Member.Name));
var newBody = pred.Body;
foreach (var me in oldMemberExprs.Zip(newMemberExprs))
newBody = newBody.Replace(me.First, me.Second);
// p => {newBody}
return Expression.Lambda<Func<TBig,bool>>(newBody, newParm);
}
/// <summary>
/// Collects all the MemberExpressions in an Expression
/// </summary>
/// <param name="e">The original Expression.</param>
/// <returns>List<MemberExpression></returns>
public static List<MemberExpression> CollectMemberExpressions<T>(this T e) where T : Expression {
new CollectMemberVisitor().Visit(e);
return CollectMemberVisitor.MemberExpressions;
}
/// <summary>
/// ExpressionVisitor to collect all MemberExpressions.
/// </summary>
public class CollectMemberVisitor : ExpressionVisitor {
public static List<MemberExpression> MemberExpressions;
public CollectMemberVisitor() {
MemberExpressions = new List<MemberExpression>();
}
protected override Expression VisitMember(MemberExpression node) {
MemberExpressions.Add(node);
return node;
}
}
}

Related

Exposing database field to 2 properties

Is there a way to expose 1 column from the database to 2 properties in a model class?
Example use case [NOT WORKING, HYPOTHETICAL EXAMPLE]:
public interface ISearcheable
{
public string SearcheableField { get; set; }
}
public class User : ISearcheable
{
public string Username { get; set; }
[DisplayName("Username")]
public string SearcheableField { get; set; }
}
public class Book : ISearcheable
{
public string Title { get; set; }
[DisplayName("Title")]
public string SearcheableField { get; set; }
}
Now if I wanted to search on each, I could just use:
db.entity<T>.Where(w => w.SearcheableField == "Username to check")
With a good bit of coding, some help from #AaronV over in How to specify dynamic field names in a Linq where clause? I was able to get this put together.
You will need to create a custom SearchableProperty attribute and apply it to the correct property in your model. Then extend Linq's IQueryable by adding a Search method. This Search method will take as parameters the type of search you want to do (being Equal to, Contains, Greater than, etc.) and the value you are searching for. The Search method will need to use reflection to look over the properties in your model and find the property that has the SearchableProperty attribute. The Search method will then dynamically build a .Where clause using that property, the operator you passed and the search value you passed.
First, create a custom SearchableProperty Attribute.
[System.AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
sealed class SearchableProperty : Attribute
{
public SearchableProperty() { }
}
Second, apply this attribute to your Model classes:
class Book
{
[Key]
public int Id { get; set; }
[SearchableProperty]
public string Author { get; set; }
}
Third, Add an enum of type Operator to your project:
public enum Operator
{
Gt,
Lt,
Eq,
Le,
Ge,
Ne,
Contains,
StartsWith,
EndsWith
}
Fourth , Use the following Search Linq Extension:
public static class LinqExtensions
{
/// <summary>
/// Queries the database for the entity which matches the provided search parameters.
/// </summary>
/// <typeparam name="TEntity">The type of entity being searched for.</typeparam>
/// <typeparam name="TDataType">The data type of the value being searched for.</typeparam>
/// <param name="op">The <see cref="Operator"/> operation that qualifies a match.</param>
/// <param name="query">The query that must be matched for the entity to be returned. </param>
/// <returns>A filtered sequence of elements. </returns>
internal static IQueryable<TEntity> Search<TEntity, TDataType>(this IQueryable<TEntity> #this, Operator op, TDataType query)
{
// Get the property that is supposed to be searchable.
PropertyInfo[] pInfo =
typeof(TEntity)
.GetProperties()
.Where(prop => prop.GetCustomAttribute<SearchableProperty>() != null)
.ToArray();
if (pInfo.Length != 1)
{
throw new InvalidOperationException($"Unable to search entity of type: {typeof(TEntity)} as the entity exposes none or more than one properties with SearchableProperty Attribute");
}
else
{
PropertyInfo searchableProperty = pInfo[0];
var dynamicExpression = CreateDynamicExpression<TEntity, TDataType>(
searchableProperty.Name,
op,
query);
return #this.Where(dynamicExpression);
}
}
/// <summary>
/// A method to create an expression dynamically given a generic entity, and a propertyName, operator and value.
/// </summary>
/// <typeparam name="TEntity">
/// The class to create the expression for. Most commonly an entity framework entity that is used
/// for a DbSet.
/// </typeparam>
/// <param name="propertyName">The string value of the property.</param>
/// <param name="op">An enumeration type with all the possible operations we want to support.</param>
/// <param name="value">A string representation of the value.</param>
/// <returns>An expression that can be used for querying data sets</returns>
private static Expression<Func<TEntity, bool>> CreateDynamicExpression<TEntity, TValueType>(string propertyName,
Operator op, TValueType value)
{
Type type = typeof(TEntity);
var p = Expression.Parameter(type, "x");
var property = Expression.Property(p, propertyName);
MethodInfo method;
Expression q;
switch (op)
{
case Operator.Gt:
q = Expression.GreaterThan(property, Expression.Constant(value));
break;
case Operator.Lt:
q = Expression.LessThan(property, Expression.Constant(value));
break;
case Operator.Eq:
q = Expression.Equal(property, Expression.Constant(value));
break;
case Operator.Le:
q = Expression.LessThanOrEqual(property, Expression.Constant(value));
break;
case Operator.Ge:
q = Expression.GreaterThanOrEqual(property, Expression.Constant(value));
break;
case Operator.Ne:
q = Expression.NotEqual(property, Expression.Constant(value));
break;
case Operator.Contains:
method = typeof(string).GetMethod("Contains", new[] { typeof(string) });
q = Expression.Call(property, method ?? throw new InvalidOperationException(),
Expression.Constant(value, typeof(string)));
break;
case Operator.StartsWith:
method = typeof(string).GetMethod("StartsWith", new[] { typeof(string) });
q = Expression.Call(property, method ?? throw new InvalidOperationException(),
Expression.Constant(value, typeof(string)));
break;
case Operator.EndsWith:
method = typeof(string).GetMethod("EndsWith", new[] { typeof(string) });
q = Expression.Call(property, method ?? throw new InvalidOperationException(),
Expression.Constant(value, typeof(string)));
break;
default:
throw new ArgumentOutOfRangeException(nameof(op), op, null);
}
return Expression.Lambda<Func<TEntity, bool>>(q, p);
}
}
Finally, use the magic:
using (DemoDbContext _context = new DemoDbContext())
{
// Sample where query to show the base sql that EF generates.
var whereQuery = _context.People.Where(x => x.Name == "John");
// Sql using the new Search method.
var searchQuery = _context.People.Search(Operator.Eq, "John");
// Sql using the new search method.
var bookSearch = _context.Books.Search(Operator.Contains, "John");
// Log everything to the console for viewing.
Console.WriteLine(whereQuery.ToQueryString());
Console.WriteLine("------------------------------------");
Console.WriteLine(searchQuery.ToQueryString());
Console.WriteLine("------------------------------------");
Console.Write(bookSearch.ToQueryString());
Console.ReadKey();
}
I would use some code similar to the following:
public interface ISearcheable
{
// This is a read-only property.
public string SearcheableField{get;}
}
public class User:ISearcheable{
public string Username {get;set;}
// Let this return the property that you want to search by and do not map it to the database.
[NotMapped]
public string SearcheableField
{
get { return Username; }
}
}
public class Book:ISearcheable{
public string Title {get;set;}
// Let this return the property that you want to search by and do not map it to the database.
[NotMapped]
public string SearcheableField
{
get { return Title; }
}
}
This would avoid duplicate of data and fields in the database.
However, just be mindful of this approach. For Example, a SQL Select statement that said SELECT author FROM book WHERE book.Title LIKE '%TEST%' would work, in Entity Framework LINQ-to-Sql this could produce unexpected results if using the SearchableField but the Username (for example) is not included in the LINQ. This should only be a concern if using Dynamic Linq and selecting only specific properties.
Also note I would personally call this field SearchPropertyValue.
I don't know if you can map the same table column to two properties on your model but I'm quite sure it's not the right way to do it.
For example, suppose the column "user_name" of the database is mapped in both User properties. Now look at this code:
var user = db.Entity<User>().First(); // Username = "Niko", SearcheableField = "Niko"
user.Username = "Joe";
var list = db.Entity<User>().Where(w=> w.SearcheableField == "Joe");
Now, what do you expect to have in your database? Joe or Niko?
And in the list array, do you want the user you just changed the name?
From my point of view, is better if you use a different approach to the problem.
For example, if you can query the data in memory, you can use this approach:
public interface ISearcheable {
string SearcheableField { get; }
}
public class User : ISearcheable {
public string Username { get; set; }
public string SearcheableField
{
get
{
return Username;
}
}
}
If you can't and won't change your DB for save a new column, you can use an Extension Method to it:
public static IQueryable<User> Search(this IQueryable<Usery> query, string value)
{
return query.Where(x => x.Username == value);
}
And then you can use it in this way:
db.Entity<User>().AsQueryable().Search("username to check");
Pay attention: I wrote it by hand and whith method names I remember. There can be few errors in synstax and code can't compile.

Filtering but property and child entity property

I got a small problem with building dynamic expression tree for search logic. Creating an expression tree for entity own properties is working fine, but I've no idea how to add an expression which will filter by child entity properties.
Here is my Task entity:
public class Task: Entity
{
public TaskType Type { get; set; }
public TaskPriority Priority { get; set; }
public int ProjectId { get; set; }
public Project Project { get; set; }
}
And here is Project entity:
public class Project: Entity
{
public int CustomerId { get; set; }
public Customer Customer { get; set; }
}
And logic for building dynamic expression:
public Func<TaskItem, bool> Build(IList<Filter> filters)
{
ParameterExpression param = Expression.Parameter(typeof(TaskItem), "task");
List<Filter> priorityFilter = FilterFilters(filters, "Priority");
List<Filter> typeFilter = FilterFilters(filters, "Type");
List<Filter> customerFilter = FilterFilters(filters, "CustomerId");
Expression expression = null;
// BuildExpression is a method which simply creates expression which is using Tasks properties (like Type or Priority)
expression = BuildExpression(param, priorityFilter, expression);
expression = BuildExpression(param, typeFilter, expression);
// This part need's to be reworked
ParameterExpression projectParam = Expression.Parameter(typeof(Project), "project");
Expression projectCustomerExpression = Expression.Equal(Expression.PropertyOrField(projectParam, "CustomerId"), Expression.Constant(customerFilter[0].Value));
Expression customerExpression = Expression.Equal(Expression.PropertyOrField(param, "Project"), projectCustomerExpression);
Expression finall = expression != null ? Expression.AndAlso(expression, projectCustomerExpression) : projectCustomerExpression;
// End of filtering by CutomerId
return Expression.Lambda<Func<TaskItem, bool>>(finall, param).Compile();
}
I've no idea how to filter by CustomerId. The part above code marked as This part need's to be reworked is probably wrong or at least the last part of it. The idea is to extend existing expression (this one build by BuildExpression method) with an expression which will filter by CustomerId.
I already lost some time on this, trying on my own and looking for answers but with no results.
Any help?
As you have provided a minimal code, how you are creating the actual expressions is unknown. So, I will try to provide a general recipe for this scenario.
If you want to filter a list of Task then you still need to use the same ParameterExpression of type Task, like you have already done:
ParameterExpression param = Expression.Parameter(typeof(TaskItem), "task");
There is no need to create another ParameterExpression of type Project even if you want to filter on properties of Project. In stead you just need to reuse the former ParameterExpression. Note that if we build a static predicate like below, this is also the case that we also don't use a different parameter expression:
queryableTask.Where(t => t.Priority == TaskPriority.High && t.Project.CustomerId == 123);
Now to dynamically build filter on navigational (child) property, the key is to form the left expression (i.e. expression for navigational property) correctly.
Lets say our navigational property is in dot notation: Project.CustomerId. Then we can do something like this to create the left expression for property:
// We already have the following commented properties
// prop = "Project.CustomerId";
// ParameterExpression param = Expression.Parameter(typeof(TaskItem), "task");
var leftExpr = prop.Split('.')
.Aggregate<string, MemberExpression>(null,
(acc, p) => acc == null
? Expression.Property(param, p)
: Expression.Property(acc, p));
And then you can do the rest like a normal property, such as creating the right expression and combining them with another expression defining the operator (Equal, Not Equal etc.).
Hope this helps.

Get Expression<Func<T, object>> for each property accessed by Expression<Func<T, U>>

I have a data access class that acts as an intermediary between logic classes and the underlying datasource, which is interchangeable. This class allows you to query the datasource using lambdas, LINQ-style. A source-agnostic class provides high-level functionality powered by a few basic operations (Add, GetAll, Update, Delete, Commit) that are implemented by small adapter classes, one for each source type (SQL, SQlite, XML serialiser, WCF client, REST client, whatever).
My problem is that some relational data sources (particularly SQLite) aren't smart enough to load relationship properties when I need them; I have to explicitly ask for them to be included. This is fine for my Get methods; I can pass a params array of expressions to load anything I need. With .Any(), however, this feels a bit odd - if I'm asking if there are any Customer records whose Purchases list contains a certain item, I shouldn't then have to tell it to load the Purchases list; that seems like the sort of thing it should be able to figure out.
So my Any() method takes Expression<Func<T, bool>> where T is obviously going to be the type I'm operating on. In the above example, it'd be used something like this:
using (var db = _dataAccessProvider.NewTransaction())
{
return db.Any<Customer>(c => c.Purchases.Contains(someProduct));
}
Is it possible to take the Expression<Func<Customer, bool>> that represents the operation c => c.Purchases.Contains(someProduct)) and work out that the property it's referring to is c => c.Purchases? How would I go about doing that? What about a lambda that touches multiple properties?
Use ExpressionVisitor to find all MemberExpression expressions which reference required object properties.
Quick example:
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
class Program
{
sealed class ReferencedPropertyFinder : ExpressionVisitor
{
private readonly Type _ownerType;
private readonly List<PropertyInfo> _properties = new List<PropertyInfo>();
public ReferencedPropertyFinder(Type ownerType)
{
_ownerType = ownerType;
}
public IReadOnlyList<PropertyInfo> Properties
{
get { return _properties; }
}
protected override Expression VisitMember(MemberExpression node)
{
var propertyInfo = node.Member as PropertyInfo;
if(propertyInfo != null && _ownerType.IsAssignableFrom(propertyInfo.DeclaringType))
{
// probably more filtering required
_properties.Add(propertyInfo);
}
return base.VisitMember(node);
}
}
private static IReadOnlyList<PropertyInfo> GetReferencedProperties<T, U>(Expression<Func<T, U>> expression)
{
var v = new ReferencedPropertyFinder(typeof(T));
v.Visit(expression);
return v.Properties;
}
sealed class TestEntity
{
public int PropertyA { get; set; }
public int PropertyB { get; set; }
public int PropertyC { get; set; }
}
static void Main(string[] args)
{
Expression<Func<TestEntity, int>> expression =
e => e.PropertyA + e.PropertyB;
foreach(var property in GetReferencedProperties(expression))
{
Console.WriteLine(property.Name);
}
}
}

How do I get property names of a type using a lambda expression and anonymous type?

I am trying to use Expression Trees and anonymous types to achieve the following.
Let's say I have this class:
class Person
{
public string FirstName {get;set;}
public string MiddleName {get;set;}
public string LastName {get;set;}
public DateTime DateOfBirth {get;set;}
}
Now I want to be able to call the following:
string[] names = Foo<Person>(x=> new { x.LastName, x.DateOfBirth });
I want names to contain 2 items, "LastName" and "DateOfBirth".
I am trying to extend PetaPoco, in a compile time safe way rather than writing string sql, so that I can specify a list of properties/columns I want to include in the SQL, rather than it selecting everything. I have some pretty large entities and there are cases where I do not want to select all the columns for performance reasons.
Try this out for size:
public static string[] Foo<T, TResult>(Expression<Func<T, TResult>> func)
{
return typeof(TResult).GetProperties().Select(pi => pi.Name).ToArray();
}
As you are returning an anonymous type from your lamda, you are able loop over all the properties of this anonymous type and use the inferred names of the properties. However when using this the syntax would be more like:
Foo((Person x) => new { x.LastName, x.DateOfBirth });
This is because the second generic argument is an anoymous type.
I'm lazy so this code handles only public properties. But it should be a good base to get you started.
public static string[] Foo<T>(Expression<Func<T, object>> func)
{
var properties = func.Body.Type.GetProperties();
return typeof(T).GetProperties()
.Where(p => properties.Any(x => p.Name == x.Name))
.Select(p =>
{
var attr = (ColumnAttribute) p.GetCustomAttributes(typeof(ColumnAttribute), true).FirstOrDefault();
return (attr != null ? attr.Name : p.Name);
}).ToArray();
}
The answers given here work when either only single property is selected, OR when multiple properties are selected. None of them work for both. The answer by Lukazoid only works for multiple properties, the rest for single property, as of writing my answer.
The code below considers both the case, that is, you can use it for selecting single property AND multiple properties. Please note that I haven't added any sanity checking here, so feel free to add your own.
string[] Foo<T>(Expression<Func<Person, T>> func)
{
if (func.Body is NewExpression)
{
// expression selects multiple properties,
// OR, single property but as an anonymous object
// extract property names right from the expression itself
return (func.Body as NewExpression).Members.Select(m => m.Name).ToArray();
// Or, simply using reflection, as shown by Lukazoid
// return typeof(T).GetProperties().Select(p => p.Name).ToArray();
}
else
{
// expression selects only a single property of Person,
// and not as an anonymous object.
return new string[] { (func.Body as MemberExpression).Member.Name };
}
}
Or more succinctly, using a ternary operator it all becomes just this:
string[] Foo<T>(Expression<Func<Person, T>> func)
{
return (func.Body as NewExpression) != null
? typeof(T).GetProperties().Select(p => p.Name).ToArray()
: new string[] { (func.Body as MemberExpression).Member.Name };
}
Download LinkPad file: LinkPad
See it online: Repl.it
Please feel free to point out anything that I may have missed.
A page of code is a thousand words, so here's how Microsoft does it in Prism:
///<summary>
/// Provides support for extracting property information based on a property expression.
///</summary>
public static class PropertySupport
{
/// <summary>
/// Extracts the property name from a property expression.
/// </summary>
/// <typeparam name="T">The object type containing the property specified in the expression.</typeparam>
/// <param name="propertyExpression">The property expression (e.g. p => p.PropertyName)</param>
/// <returns>The name of the property.</returns>
/// <exception cref="ArgumentNullException">Thrown if the <paramref name="propertyExpression"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when the expression is:<br/>
/// Not a <see cref="MemberExpression"/><br/>
/// The <see cref="MemberExpression"/> does not represent a property.<br/>
/// Or, the property is static.
/// </exception>
public static string ExtractPropertyName<T>(Expression<Func<T>> propertyExpression)
{
if (propertyExpression == null)
{
throw new ArgumentNullException("propertyExpression");
}
var memberExpression = propertyExpression.Body as MemberExpression;
if (memberExpression == null)
{
throw new ArgumentException(Resources.PropertySupport_NotMemberAccessExpression_Exception, "propertyExpression");
}
var property = memberExpression.Member as PropertyInfo;
if (property == null)
{
throw new ArgumentException(Resources.PropertySupport_ExpressionNotProperty_Exception, "propertyExpression");
}
var getMethod = property.GetGetMethod(true);
if (getMethod.IsStatic)
{
throw new ArgumentException(Resources.PropertySupport_StaticExpression_Exception, "propertyExpression");
}
return memberExpression.Member.Name;
}
}
If you want to take attributes into account it's going to be slightly more complicated, but the general idea of accepting an Expression<Func<T>> and fishing out the name of the property being targeted is the same.
Update: As is, the method will accept only one parameter; I only provided it as a guideline. The idea can be generalized of course:
public static string[] ExtractPropertyNames<T>(
Expression<Func<T, object>> propertyExpression)
This method will accept an expression that takes a T and returns an anonymous type which you can then reflect upon. You could substitute a second type parameter for object but that doesn't really do anything here because the only thing you want to do is reflect on the type.
I guess you have to disassemble code for Html.LabelFor(LabelExtensions.LabelFor<TModel,TValue> from System.Web.Mvc assembly).
For example, look at ExpressionHelper.GetExpressionText
As for replacing member name with attribute member value - you'll have to use old fashioned reflection.

Nested Lambda for use with Fluent Interface

Given the following types:
class Parent { List<Child> Children {get;set;}}
class Child {List<Child> GrandChildren {get;set;}}
class Helper<TEntity> {List<string> Properties {get;set;}}
And given the following methods on Helper...
public Helper AddProps<TEntity, TProp>(Expression<Func<TEntity, TProp>> exp)
{
this.Properties.Add(GetPropInfo(exp).Name);
}
public PropertyInfo GetPropInfo(Expression<Func<TEntity, TProp>> exp)
{
return (PropertyInfo)((MemberExpression)(expression.Body)).Member;
}
I am able to do this:
Helper<Parent> myHelper = new Helper<Parent>();
myHelper.AddProps(x => x.Children);
The string list "Properties" on myHelper would then contain the value "Children", the name of the property passed through the expression.
What I want to do now is to be able to achieve the same thing, only with the ability to reflect type hierarchy.
Would it look like this ?
x => x.Children { xx => xx.GrandChildren }
Or is it even possible, and what would be involved? I've seen nested lambda's before but don't know what's involved.
Thanks in advance!
EDIT
It seems there is some confusion so I'll try to clarify. I want to be able to create a string that looks like this "Object.SubObject.SubSubObject" using lambda expressions and method chaining. My example does this, but only for one level deep ( a property of a class). What I want to do is extend this to go to any depth.
For Example, I'd like to use lambda expressions with a fluent interface that would look something like this....
AddProps(x => x.Children).AddProps(xx => xx.GrandChildren) and that would add "Children.GrandChildren" to my "Properties" string list.
It might make things easier if the AddProps method is generic, instead of the entire Helper Class.
In this way you could have this code:
var helper = new Helper();
helper.AddProps<Parent>(x => x.Children);
helper.AddProps<Child>(x => x.GrandChildren);
You might also want to retain more than just the property name so that you know which type the property belongs to. I guess you could have a Dictionary to store the properties for each type you've registered.
P.S. Knowing why you want to do this might help the SO community better answer your question.
I ended up using an alternate solution that worked quite well. It uses this new class...
public class PartBuilder
{
private List<string> Parts;
/// <summary>
/// Gets a dot delimited string representing the parts
/// </summary>
public string Value
{
get
{
return string.Join(".", this.Parts.ToArray());
}
}
/// <summary>
/// Creates a new PartBuilder
/// </summary>
private PartBuilder()
{
this.Parts = new List<string>();
}
/// <summary>
/// Creates a new PartBuilder
/// </summary>
public static PartBuilder Create()
{
return new PartBuilder();
}
/// <summary>
/// Gets a property name from an expression
/// </summary>
public PartBuilder AddPart<TEntity, TProp>(Expression<Func<TEntity, TProp>> expression)
{
PropertyInfo prop = (PropertyInfo)((MemberExpression)(expression.Body)).Member;
this.Parts.Add(prop.Name);
return this;
}
}
Now that I have this new class, I can do this...
string typeHierarchy = PartBuilder.Create()
.AddPart((Parent p) => p.Children)
.AddPart((Child pp) => pp.GrandChildren)
.Value;
The variable "typeHierarchy" now has the value of "Children.GrandChildren".
It's not as elegant as I would have hoped, but it's type safe and easy to use.

Categories

Resources