views:

79

answers:

2

I'm trying to create a generic repository for my models. Currently i've 3 different models which have no relationship between them. (Contacts, Notes, Reminders).

class Repository<T> where T:class
{
    public IQueryable<T> SearchExact(string keyword)
    {
        //Is there a way i can make the below line generic
        //return db.ContactModels.Where(i => i.Name == keyword)        
        //I also tried db.GetTable<T>().Where(i => i.Name == keyword)
        //But the variable i doesn't have the Name property since it would know it only in the runtime
        //db also has a method ITable GetTable(Type modelType) but don't think if that would help me
    }
}

In MainViewModel, I call the Search method like this:

Repository<ContactModel> _contactRepository = new Repository<ContactModel>();

public void Search(string keyword)
{
    var filteredList = _contactRepository.SearchExact(keyword).ToList();
}

Solution:

Finally went with Ray's Dynamic Expression solution:

public IQueryable<TModel> SearchExact(string searchKeyword, string columnName)
{
    ParameterExpression param = Expression.Parameter(typeof(TModel), "i");
    Expression left = Expression.Property(param, typeof(TModel).GetProperty(columnName));
    Expression right = Expression.Constant(searchKeyword);
    Expression expr = Expression.Equal(left, right);
}

query = db.GetTable<TModel>().Where(Expression.Lambda<Func<TModel, bool>>(expr, param));
+7  A: 

Interface solution

If you can add an interface to your object you can use that. For example you could define:

 public interface IName
 {
   string Name { get; }
 }

Then your repository could be declared as:

class Repository<T> where T:class, IName
{
  public IQueryable<T> SearchExact(string keyword)  
  {  
    return db.GetTable<T>().Where(i => i.Name == keyword);
  }
}  

Alternate interface solution

Alternatively you could put the "where" on your SearchExact method by using a second generic parameter:

class Repository<T> where T:class
{  
  public IQueryable<T> SearchExact<U>(string keyword) where U: T,IName
  {  
    return db.GetTable<U>().Where(i => i.Name == keyword);
  }
}  

This allows the Repository class to be used with objects that don't implement IName, whereas the SearchExact method can only be used with objects that implement IName.

Reflection solution

If you can't add an IName-like interface to your objects, you can use reflection instead:

class Repository<T> where T:class
{
  static PropertyInfo _nameProperty = typeof(T).GetProperty("Name");

  public IQueryable<T> SearchExact(string keyword)
  {
    return db.GetTable<T>().Where(i => (string)_nameProperty.GetValue(i) == keyword);
  }
}

This is slower than using an interface, but sometimes it is the only way.

More notes on interface solution and why you might use it

In your comment you mention that you can't use an interface but don't explain why. You say "Nothing in common is present in the three models. So i think making an interface out of them is not possible." From your question I understood that all three models have a "Name" property. In that case, it is possible to implement an interface on all three. Just implement the interface as shown and ", IName" to each of your three class definitions. This will give you the best performance for both local queries and SQL generation.

Even if the properties in question are not all called "Name", you can still use the nterface solution by adding a "Name" property to each and having its getter and setter access the other property.

Expression solution

If the IName solution won't work and you need the SQL conversion to work, you can do this by building your LINQ query using Expressions. This more work and is significantly less efficient for local use but will convert to SQL well. The code would be something like this:

class Repository<T> where T:Class
{
  public IQueryable<T> SearchExact(string keyword,
                                   Expression<Func<T,string>> getNameExpression)
  {
    var param = Expression.Parameter(typeof(T), "i");
    return db.GetTable<T>().Where(
                Expression.Lambda<Func<T,bool>>(
                  Expression.Equal(
                    Expression.Invoke(
                      Expression.Constant(getNameExpression),
                      param),
                    Expression.Constant(keyword),
                  param));
  }
}

and it would be called thusly:

repository.SearchExact("Text To Find", i => i.Name)
Ray Burns
Thinking laterally, I like it. Linq2Sql will mean that they will probably need to use some post processing to apply the interface to the classes.
Spence
Second approach (putting constraint on method) seems better to me, but would probably then go on to rename the method SearchExactName.
David_001
@Spence - there are two ways of doing this; you can either use partial classes to add the `: IName`, or you can edit the DBML and set the global base-type for entities (if all your types should implement it).
Marc Gravell
Yes, I sometimes used partial classes for this when I had to use Linq2Sql.
Ray Burns
@Ray Burns: Thanks for the quick reply. Nothing in common is present in the three models. So i think making an interface out of them is not possible. Instead can i give the scope like this `where U : T, ContactModel, NoteModel, ReminderModel` to achieve what i want? I'm really weak in reflections. Can you explain that part also?
Veer
The where clause requires ALL the specified criteria, so that statement would require it to be or inherit from all 4 types specified (T, ContactModel, NoteModel and ReminderModel), so this method simply will not work without the interface. Also as a sidenote the reflection solution currently says it requires the interface but simply remove the `where U: T,IName` and swap the U for T (I assume that's just a copy-paste)
Tim Schneider
No `where U : T, ContactModel, NoteModel, ReminderModel` won't work because U would have to be all three classes at once. Did you notice that I gave you actual code for the reflection solution? I'm not sure there's much to explain about it. var prop = `typeof(T).GetProperty("Name")` finds a property called "Name" on the target object and returns an object that can rapidly access the Name value. Inside the Where, `prop.GetValue(i)` actually retrieves the name. It's pretty straightforward and relatively fast.
Ray Burns
@Tim: Thanks for catching that. You're right - it was copy/paste. I have corrected my answer.
Ray Burns
I also made the reflection code more efficient by looking up the property in the .cctor and caching it in a static field.
Ray Burns
@Ray Burns: I saw the reflection solution just after i've posted the comment.
Veer
+3  A: 

Ray's method is quite good, and if you have the ability to add an interface definitely the superior however if for some reason you are unable to add an interface to these classes (Part of a class library you can't edit or something) then you could also consider passing a Func in which could tell it how to get the name.

EG:

class Repository<T>
{  
  public IQueryable<T> SearchExact(string keyword, Func<T, string> getSearchField)  
  {  
    return db.GetTable<T>().Where(i => getSearchField(i) == keyword);
  }
}

You'd then have to call it as:

var filteredList = _contactRepository.SearchExact(keyword, cr => cr.Name).ToList();

Other than these two options you could always look into using reflection to access the Name property without any interface, but this has the downside that there's no compile-time check that makes sure the classes you're passing actually DO have a Name property and also has the side-effect that the LINQ will not be translated to SQL and the filtering will happen in .NET (Meaning the SQL server could get hit more than is needed).

You could also use a Dynamic LINQ query to achieve this SQL-side effect, but it has the same non type-safe issues listed above.

Tim Schneider
+1 for using the alternative functional approach, rather than OO approach of Ray's solution.
David_001
@Tim Schneider: +1 That should be the solution. Let me confirm today.
Veer
Yes, Func is another good way to do it. Note that it has the same disadvantage as reflection does in terms of translation: Because `getSearchField` is already a delegate (a pointer to actual machine code), LINQ2SQL has no way of parsing it, so again the filtering will happen in .NET. The interface approach will successfully translate to SQL, but to get this to translate you must arrange to pass in an Expression, but this will make local execution slower.
Ray Burns
@Ray Burns/@Tim: one thing i'm very sure is that i need the translation to SQL. I want to query the db and hence i need to return a `IQueryable<T>`. I call up the enumerator by invoking the dispatcher and populating my listview asynchronously. Which solution would then work for me?
Veer
I added several paragraphs and one more solution to my answer to help answer this. I explain that the interface solution doesn't seem incompatible with anything you have said so far, and also give a new Expression solution that is much less efficient and more code, but will work if the interface solution is actually infeasible.
Ray Burns
You're right Ray, the func method won't translate to SQL either. The dynamic expression approach shown by Ray will work, but you're getting into some very messy code and I've yet to see any reason you wouldn't use the simple interface method.
Tim Schneider
@Ray/@Tim: I can't use the Interface method because the Name property is not present in all the three models. Even if I've a property called Title which gets the name from Contact, subject from reminder and Title from Note and create an Interface, that could be used to search only that particular field. But just for eg. I've shown that method which searches only Name field. But there could be other methods also in the repository which would access different fields in different models. I can't create common prop for other fields as the type and no. of fields won't be the same in all the models.
Veer
@Ray/@Tim: As you see, this is just to make the Reposity generic. Just have a single class for all the models. Usually we would have ContactRepository, NoteRepository and ReminderRepository. If I add another model in the future, I've to add another Repository. Moreover, the usual CRUD operation is repeated in all these repositories. Only this Search operation and other jobs that requires accessing a particular field is problematic as they can't be generic. I think you understood the problem better.
Veer
@Ray/@Tim: The alternative could be to use an IRepository having the basic CRUD methods, implement it in every class explicity and add the extra class specific methods. I want you to suggest if making it generic the correct way to do it or i should stick to IRepository.
Veer
How about using a dynamic keyword. `dynamic dTable = db.GetTable<T>(); dTable.Where(i => i.Name == keyword);`
Veer
The dynamic keyword has the same problem as the Func and the Reflection solutions: LINQ2SQL won't translate it to SQL. The best of these three is the Func solution. Since you need translation to SQL you'll have to choose between the Interface solution and the Expression solution.
Ray Burns