I have repositories (e.g. ContactRepository, UserRepository and so forth) which encapsulate data access to the domain model.
When I was looking at searching for data, e.g.
a contact whose birthday is after 1960
(etc),
I started implementing repository methods such as FirstNameStartsWith(string prefix) and YoungerThanBirthYear(int year), basically following the many examples out there.
Then I hit a problem - what if I have to combine multiple searches? Each of my repository search methods, such as above, only return a finite set of actual domain objects. In search for a better way, I started writing extension methods on IQueryable<T>, e.g. this:
public static IQueryable<Contact> FirstNameStartsWith(
this IQueryable<Contact> contacts, String prefix)
{
return contacts.Where(
contact => contact.FirstName.StartsWith(prefix));
}
Now I can do things such as
ContactRepository.GetAll().FirstNameStartsWith("tex").YoungerThanBirthYear(1960);
However, I found myself writing extension methods (and inventing crazy classes such as ContactsQueryableExtensions all over, and I lose the "nice grouping" by having everything in the appropriate repository.
Is this really the way to do it, or is there a better way to achieve the same goal?
I have been thinking about this a lot lately, after starting at my current job. I am used to Repositories, they go the full IQueryable path using just bare bones repositories as you suggest.
I feel the repo pattern is sound and does a semi-effective job at describing how you want to work with the data in the application domain. However the issue you are describing definitely occurs. It gets messy, fast, beyond a simple application.
Are there, perhaps, ways to rethink why you are asking for the data in so many ways? If not, I really feel that a hybrid approach is the best way to go. Create repo methods for the stuff you reuse. Stuff that actually it makes sense for. DRY and all that. But those one-offs? Why not take advantage of IQueryable and the sexy things you can do with it? It is silly, as you said, to create a method for that, but it doesn't mean you don't need the data. DRY doesn't really apply there does it?
It would take discipline to do this well, but I really think it's an appropriate path.
@Alex - i know this is an old question, but what I would be doing would be letting the Repository do really simple stuff only. This means, get all records for a table or view.
Then, in the SERVICES layer (you are using an n-tiered solution, right? :) ) i would be handling all the 'special' query stuff there.
Ok, example time.
ContactRepository.cs
public IQueryable<Contact> GetContacts()
{
return (from q in SqlContext.Contacts
select q).AsQueryable();
}
Nice and simple. SqlContext
is the instance of your EF Context
.. which has an Entity
on it called Contacts
.. which is basically your sql Contacts class.
This means, that method basically is doing: SELECT * FROM CONTACTS
... but it's not hitting the database with that query .. it's only a query right now.
Ok .. next layer.. KICK ... up we go (Inception anyone?)
ContactService.cs
public ICollection<Contact> FindContacts(string name)
{
return FindContacts(name, null)
}
public ICollection<Contact> FindContacts(string name, int? year)
{
IQueryable<Contact> query = _contactRepository.GetContacts();
if (!string.IsNullOrEmpty(name))
{
query = from q in query
where q.FirstName.StartsWith(name)
select q;
}
if (int.HasValue)
{
query = from q in query
where q.Birthday.Year <= year.Value
select q);
}
return (from q in query
select q).ToList();
}
Done.
So lets recap. First, we start our with a simple 'Get everything from contacts' query. Now, if we have a name provided, lets add a filter to filter all contacts by name. Next, if we have a year provided, then we filter the birthday by Year. Etc. Finally, we then hit the DB (with this modified query) and see what results we get back.
NOTES:-
Good luck :)
I realize this is old, but I've been dealing with this same issue lately, and I came to the same conclusion as Chad: with a little discipline, a hybrid of extension methods and repository methods seems to work best.
Some general rules I've been following in my (Entity Framework) application:
If the method is used only for ordering, I prefer to write extension methods that operate on IQueryable<T>
or IOrderedQueryable<T>
(to leverage the underlying provider.) e.g.
public static IOrderedQueryable<TermRegistration> ThenByStudentName(
this IOrderedQueryable<TermRegistration> query)
{
return query
.ThenBy(reg => reg.Student.FamilyName)
.ThenBy(reg => reg.Student.GivenName);
}
Now I can use ThenByStudentName()
as needed within my repository class.
If the method involves querying by primitive parameters, it usually requires an ObjectContext
and can't be easily made static
. These methods I leave on my repository, e.g.
public Student GetById(int id)
{
// Calls context.ObjectSet<T>().SingleOrDefault(predicate)
// on my generic EntityRepository<T> class
return SingleOrDefault(student => student.Active && student.Id == id);
}
However, if the method instead involves querying an EntityObject
using its navigation properties, it can usually be made static
quite easily, and implemented as an extension method. e.g.
public static TermRegistration GetLatestRegistration(this Student student)
{
return student.TermRegistrations.AsQueryable()
.OrderByTerm()
.FirstOrDefault();
}
Now I can conveniently write someStudent.GetLatestRegistration()
without needing a repository instance in the current scope.
If the method returns some IEnumerable
, ICollection
or IList
, then I like to make it static
if possible, and leave it on the repository even if it uses navigation properties. e.g.
public static IList<TermRegistration> GetByTerm(Term term, bool ordered)
{
var termReg = term.TermRegistrations;
return (ordered)
? termReg.AsQueryable().OrderByStudentName().ToList()
: termReg.ToList();
}
This is because my GetAll()
methods already live on the repository, and it helps to avoid a cluttered mess of extension methods.
Another reason for not implementing these "collection getters" as extension methods is that they would require more verbose naming to be meaningful, since the return type isn't implied. For example, the last example would become GetTermRegistrationsByTerm(this Term term)
.
I hope this helps!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With