
Almost three years have passed since I first wrote about my
refusal of such an abstraction as a repository. Since then, I have practically not used any repository concepts in the systems we are developing. I did not remove existing repositories from the projects, but now I just don’t find any value in them as abstractions.
The repositories that developers create are mostly of two kinds:
- Abstraction around the ORM framework,
- Encapsulation of requests.
An example of the first case might be something like this:
public interface IConferenceRepository { IRavenQueryable<Conference> Query(); Conference Load(Guid id); }
Encapsulating queries usually takes a few more lines:
public interface IConferenceRepository { IEnumerable<Conference> FindAll(); IEnumerable<Conference> FindFuture(); IEnumerable<Conference> FindFree(); IEnumerable<Conference> FindPaid(); }
Here each method encapsulates one request. Both cases are valuable in certain scenarios. If I have a goal - to abstract from my ORM, I will go the first way, and maybe turn on the second one too.
But is ORM something that requires abstraction? I do not think so - abstraction from something similar ORM actively prevents me from using its powerful functionality. ORM is already an abstraction, we really have to ask ourselves,
do we need an abstraction of abstraction ?
Digging deeper')
First of all, we have to return to the question, why did we begin to use the repository pattern? Surely this was done in the name of "
testability ." Then let's start with something like this:
public ActionResult Index() { RavenQueryStatistics stats; var posts = RavenSession.Query<Post>() .Include(x => x.AuthorId) .Statistics(out stats) .WhereIsPublicPost() .OrderByDescending(post => post.PublishAt) .Paging(CurrentPage, DefaultPage, PageSize) .ToList(); return ListView(stats.TotalResults, posts); }
Seems difficult? Not. Although if the complexity grows, we will still limit its scale by this method alone. If we put this request into a separate class, repository or extension method, the request itself will still be in the same method. From the point of view of the controller method, does it matter where this code is located - in the controller or another class?
How about a more complex example:
public ActionResult Archive(int year, int? month, int? day) { RavenQueryStatistics stats; var postsQuery = RavenSession.Query<Post>() .Include(x => x.AuthorId) .Statistics(out stats) .WhereIsPublicPost() .Where(post => post.PublishAt.Year == year); if (month != null) postsQuery = postsQuery.Where(post => post.PublishAt.Month == month.Value); if (day != null) postsQuery = postsQuery.Where(post => post.PublishAt.Day == day.Value); var posts = postsQuery.OrderByDescending(post => post.PublishAt) .Paging(CurrentPage, DefaultPage, PageSize) .ToList(); return ListView(stats.TotalResults, posts); }
Again, this is just a collection of queries. I still want to encapsulate it in one place, but I see no reason to move this code from where it is now. If the request changes, I will simply change the code in one place. Additional abstraction in this case can only confuse.
The nuance arises if I have several concepts with which I work in the controller method. Let's look at the controller method, which should do several things:
[ValidateInput(false)] [HttpPost] public ActionResult Comment(CommentInput input, int id, Guid key) { var post = RavenSession .Include<Post>(x => x.CommentsId) .Load(id); if (post == null || post.IsPublicPost(key) == false) return HttpNotFound(); var comments = RavenSession.Load<PostComments>(post.CommentsId); if (comments == null) return HttpNotFound(); var commenter = RavenSession.GetCommenter(input.CommenterKey); if (commenter == null) { input.CommenterKey = Guid.NewGuid(); } ValidateCommentsAllowed(post, comments); ValidateCaptcha(input, commenter); if (ModelState.IsValid == false) return PostingCommentFailed(post, input, key); TaskExecutor.ExcuteLater(new AddCommentTask(input, Request.MapTo<AddCommentTask.RequestValues>() ,id)); CommenterUtil.SetCommenterCookie(Response, input.CommenterKey.MapTo<string>()); return PostingCommentSucceeded(post, input); }
In this case, there is a lot of validation, but this work is given to the
AddCommentTask
object. This is a command object that takes care of performing the task outside MVC, validations, ActionResult, and the like.
We have made some concepts out of our abstractions (tasks, like
AddCommentTask
) and in which case we can do the same with queries.
Testing strategiesMy testing strategy today is:
- Unit testing of isolated components (domain models and other already isolated classes)
- Integration testing of everything else
I do not use auto-mocking containers. I bring components to stubs that I cannot control. Otherwise, it turns into a strategy for cramming logic deeper and deeper.
For something like a database, my tests will be slower. And I prefer to accept this because it gives me ease when refactoring. My tests don't break just because some kind of stub needs to be redone.
In my controllers, I just prefer to have an interface (
seam, seam - approx. Ed. ) For testing. In the
RaccoonBlog project
, this means that simply replacing the RavenDB storage mechanism with in-memory will make my tests much faster.
But even otherwise, I'm not worried about adding a repository. In my experience, introducing a repository just to bring something out is a waste of time. This adds an unnecessary abstraction in the place where some concept would be enough (for example, encapsulating the request object).
Instead of focusing on abstractions, I focus on concepts, and let tests fall where they can. In the end, my controllers are not object-oriented - they are procedural (as this is confirmed by the demands put on them).
Jimmy Bogard is an architect at Headsrping, the creator of AutoMapper and co-author of the book ASP.NET MVC in Action. In his blog, he focuses on DDD, CQRS, distributed systems, and related architectures and methodologies.