I'm designing an alternative MVC framework for ASP.Net. Part of my goals for the framework is to have as little "magic" as possible. The only bit of reflection I have is for binding things like form, query string, etc to a plain ol' class(with some optional attributes for ignore, conversion, etc). As such, I do absolutely no class/method detection. Everything must be very explicit. I've gone through about 3 iterations of API "shape". The first two achieved my goal of having no magic, but it was very verbose and not easy to read.. and controllers usually had to do the heavy lifting the MVC framework should do.
So, now in this third iteration I'm trying really hard to get it right. One slightly controversial thing I do differently is the routing code. Because everything is explicit and reflection is discouraged, I can't search for some attribute in the controller to resolve a route. Everything must be specified at the route level. In the first iteration this wasn't done, but it made for extremely cumbersome and verbose controllers...
Right now, I have this fluent API for specifying routes. It has gone a bit beyond what I first imagined though and now functions as a sort of way to specify what a controller's method is capable of and what it should accept.
On to the actual code. The implementation is irrelevant. The only thing you really need to know is that there is a LOT of generic types involved. So, here is a quick sample of some routing:
var router=new Router(...);
var blog=router.Controller(() => new BlogController());
blog.Handles("/blog/index").With((ctrl) => ctrl.Index());
blog.Handles("/blog/{id}").With((ctrl,model) => ctrl.View(model["id"])).WhereRouteLike((r) => r["id"].IsInteger()); //model defaults to creating a dictionary from route parameters
blog.Handles("/blog/new").UsingFormModel(() => new BlogPost()).With((ctrl, model) => ctrl.NewPost(model)); //here model would be of type BlogPost. Also, could substitue UsingRouteModel, UsingQueryStringModel, etc
There are also some other methods that could be implemented such as WhereModelIsLike
or some such that does verification on the model. However, does this kind of "specification" belong in the routing layer? What are the limits that should be specified in the routing layer? What should be left to the controller to validate?
Am I making the routing layer worry about too much?
I think the routing is way too verbose. I wouldn't want to write that kind of code for 20 controllers. Especially, because it is really repetetive.
The problem I see here that even default cases require verbose declarations. Those verbose declarations only should be needed for special cases.
It is expressive and readable, but you might want to consider packaging advanced features up.
Have a look at the following specification. And that's just for a single action in a single controller:
blog.Handles("/blog/new")
.UsingFormModel(() => new BlogPost())
.With((ctrl, model) => ctrl.NewPost(model))
.WhereModelIsLike(m => m.Status == PostStatus.New);
One way to only slightly reducing the amount of code would be to allow the specification of a root folder:
var blog=router.Controller(() => new BlogController(), "/blog");
blog.Handles("index").Wi..
blog.Handles("{id}").Wit..
blog.Handles("new").Usin..
Another idea to reduce the code for default cases would be to introduce one interface per default action. The controller needs to implement the interfaces for supported actions:
Something like this maybe:
public interface ISupportIndex
{
void Index();
}
public interface ISupportSingleItem
{
void View(int id);
}
Now, you could provide methods like blog.HandlesIndex();
, blog.HandlesSingleItem();
.
Those methods return the same thing as your existing methods, so the result can be further refined.
They could be designed as extension methods that are only available if the controller actually implements the interface. To achieve this, the return type of router.Controller
would need to be a covariant interface with the controller as generic parameter, i.e. something like this:
IControllerRoute<out TController>
For example, the extension method HandlesIndex
would be implemented like this:
public static IRouteHandler HandlesIndex(
this IControllerRoute<ISupportIndex> route)
{
// note: This makes use of the "root" as suggested above:
// It only specifies "index", not "/someroot/index".
return route.Handles("index").With(x => x.Index);
}
work on IControllerRoute<ISupportIndex>
, to be only displayed in cases the controller actually supports it.
The route for the blog controller could look like this:
blog.HandlesIndex();
blog.HandlesSingleItem();
// Uses short version for models with default constructor:
blog.HandlesNew<BlogPost>().UsingFormModel();
// The version for models without default constructor could look like this:
//blog.HandlesNew<BlogPost>().UsingFormModel(() => new BlogPost(myDependency));
Adding validation rules could be done a little bit more concise, too:
blog.HandlesNew<BlogPost>().UsingFormModel()
.When(m => m.Status == PostStatus.New);
If the specification is more complex, it could be packaged in its own class that implements IModelValidation
. That class is now used:
blog.HandlesNew<BlogPost>().UsingFormModel()
.WithValidator<NewBlogPostValidation>();
All of my suggestions are just ways to make your current approach easier to handle, so I guess up till now, it doesn't really answer your actual question. I do that now:
I like my controllers as clean as possible. Putting validation rules on the route is something that looks very good to me, because the controller action now can assume that it only is called with valid data. I would continue with this approach.