I'm currently building a web application and attempting to design it following good MVC and service-oriented architecture.
I have, however, hit a bit of a wall in connecting the presentation layer (i.e. my controllers) and the back-end services while still maintaining good error/validation reporting back to the user.
I read a really good SO post here about how to separate Validation logic from the service layer and for the most part it all made sense. However there was one "flaw", if you can call it that, in this model that niggled at me: How do you avoid duplicating effort when looking up objects that are required by both the validator and the service?
I think it'd be easier to explain with a reasonably simple example:
Let's say I have an application that allows users to share code snippets around. Now, I've decided to add a new feature which allows a user to attach their GitHub account to their account on my site (i.e. to build up a profile). For the purpose of this example I'm going to simply assume that all my users are trustworthy and would only attempt to add their own GitHub accounts, not anyone else's :)
Following the aforementioned SO article I've set up a basic GitHub service for retrieving GitHub user info.
interface IGitHubUserService {
GitHubUser FindByUserName(string username);
}
The concrete implementation of GitHubUserService makes an expensive call to https://api.github.com/users/{0}
in order to pull user information.
Again, following the article's model I implemented the following command to link a user account to a GitHub user:
// Command for linking a GitHub account to an internal user account
public class GitHubLinkCommand {
public int UserId { get; set; }
public string GitHubUsername { get; set }
};
My validator needs to validate that the username entered by the user is a valid GitHub account. This is very straightforward: call FindByUserName
on the GitHubUserService
and make sure that the result isn't null:
public sealed class GitHubLinkCommandValidator : Validator<GitHubLinkCommand> {
private readonly IGitHubUserService _userService;
public GitHubLinkCommandValidator(IGitHubUserService userService) {
this._userService = userService;
}
protected override IEnumerable<ValidationResult> Validate(GitHubLinkCommand command) {
try {
var user = this._userService.FindByUserName(command.GitHubUsername);
if (user == null)
yield return new ValidationResult("Username", string.Format("No user with the name '{0}' found on GitHub's servers."));
}
catch(Exception e) {
yield return new ValidationResult("Username", "There was an error contacting GitHub's API.");
}
}
}
Okay that's great! The validator is really straightforward and makes sense. Now it's time to make the GitHubLinkCommandHandler
:
public class GitHubLinkCommandHandler : ICommandHandler<GitHubLinkCommand>
{
private readonly IGitHubUserService _userService;
public GitHubLinkCommandHandler(IGitHubUserService userService)
{
this._userService = userService;
}
public void Handle(GitHubLinkCommand command)
{
// Get the user details from GitHub:
var user = this._userService.FindByUserName(command.GitHubUsername);
// implementation of this entity isn't really relevant, just assume it's a persistent entity to be stored in a backing database
var entity = new GitHubUserEntity
{
Name = user.Login,
AvatarUrl = user.AvatarUrl
// etc.
};
// store the entity:
this._someRepository.Save(entity);
}
}
Again, this looks really neat and straightforward. However there's one glaring issue: The duplicate calls to IGitHubUserService::FindByUserName
, one from the validator and one from the service.
On a bad day such a call can take 1-2 seconds without server-side caching, making duplication far too expensive to use this architectural model.
Has anyone else encountered such an issue when writing validators/services around external APIs and how did you reduce the duplication of effort outside of implementing a cache in your concrete class?
IGitHubUserService
from my application. There I would put a cache and even if it often acts like a Proxy (with some decoration) it may also become an Adapter (if GitHub interface changes) or even a Bridge (in case you want to make it generic enough to be used also with CodePlex, Google Code...) – Append