-
Notifications
You must be signed in to change notification settings - Fork 0
Home
This page is to describe the thought processes about why the project is in this shape.
MVC had some great concepts of separation of concerns, however they did it at a horizontal level instead of a vertical level. E.g. all controllers in one folder, all views in another folder. The problem with horizontal separation is that it makes feature development sparse. You have to constantly navigate between large bucket folders of files to find the thing you are looking for.
Feature folders takes a more vertical (or feature) based approached. Put all the things that are relevant to a feature in the same folder. This makes the navigation aspect less of a burden and feels more intuitive.
To achieve this, you need to override the razor view engine's search locations (FeatureViewLocationRazorViewEngine), and also controller location (ActionPerControllerFactory).
Both of these parts are configured within the application start (Global.asax):
SetControllerFactoryToActionPerControllerFactory(container);
SetViewEngineToFeatureFolderStructure();
Action per controller means you are only dealing with the GET/POST parts of any action. The reason for this is it keeps the controllers less bloated, and usually translates one to one with business conversations or domain interactions. This ties in nicely with validation, as you can then isolate validation rules for the actions you are performing, instead of having to know the explicit actions that caused state change within your domain.
For example, when you "Save" something, what has happened on your object? What specific conditions need to be considered when calling "Save"? All of them? What if you only had to validate the action you were performing? That creates much less thought in validation of state. Adding a new item, you really only have to be concerned with required fields, and unique values. That's only two things that need consideration in this controller's behaviour which keeps the logic clear and also will usually only have one reason to change.
Here is an example:
private readonly BrewContext _context;
public AddBrewController(BrewContext context)
{
_context = context;
}
public ActionResult Add()
{
return View();
}
[HttpPost]
public ActionResult Add(AddBrewViewModel model)
{
if (!ModelState.IsValid)
return View(model);
var brewToAdd = new Domain.Brew(model.Name);
_context.Set<Domain.Brew>().Add(brewToAdd);
_context.SaveChanges();
return RedirectToAction("Index", "Home");
}
Why fluent? It's easy to read, configure and extend. Done right, there's no obtrusive parts that need to be considered in your controller or view model designs.
Here's an example of a unique check when adding a brew:
public class AddBrewViewModelValidator : AbstractValidator<AddBrewViewModel>
{
private readonly BrewContext _brewContext;
public AddBrewViewModelValidator(BrewContext brewContext)
{
_brewContext = brewContext;
RuleFor(x => x.Name)
.Must(BeAUniqueName)
.WithMessage("That brew name is already taken; please try another.");
}
public bool BeAUniqueName(string name)
{
return !_brewContext.Brews.Any(x => x.Name == name);
}
}
Pretty simple hey? By hooking into the MVC ModelValidatorProviders, this configuration is quite trivial:
ModelValidatorProviders.Providers.Add(
new FluentValidationModelValidatorProvider(container.Resolve<IValidatorFactory>())
{
AddImplicitRequiredValidator = false
});
Of course there's some dependency injection work to get this up and running as well, but once it's done it's out the way. Check out the Infrastructure\Validation\AutofacValidatorFactory and FluentValidatorModule to see how the validators are resolved. When all of this is wired up, your validation in controller actions looks like this:
[HttpPost]
public ActionResult Add(AddBrewViewModel model)
{
if (!ModelState.IsValid)
return View(model);
...
}