Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Route Parameter, Custom Model Binder or Action Filter?

Our ASP.NET MVC application allows an authenticated user to administer one or more "sites" linked to their account.

Our Urls are highly guessible since we use the site friendly name in the URL rather than the Id e.g:

/sites/mysite/
/sites/mysite/settings

/sites/mysite/blog/posts
/sites/mysite/pages/create

As you can see we need access to the site name in a number of routes.

We need to execute the same behaviour for all of these actions:

  1. Look for a site with the given identifier on the current account
  2. If the site returned is null, return a 404 (or custom view)
  3. If the site is NOT null (valid) we can carry on executing the action

The current account is always available to us via an ISiteContext object. Here is how I might achieve all of the above using a normal route parameter and performing the query directly within my action:

private readonly ISiteContext siteContext;
private readonly IRepository<Site> siteRepository;

public SitesController(ISiteContext siteContext, IRepository<Site> siteRepository)
{
    this.siteContext = siteContext;
    this.siteRepository = siteRepository;
}

[HttpGet]
public ActionResult Details(string id)
{
    var site =
        siteRepository.Get(
            s => s.Account == siteContext.Account && s.SystemName == id
        );

    if (site == null)
        return HttpNotFound();

    return Content("Viewing details for site " + site.Name);
}

This isn't too bad, but I'm going to need to do this on 20 or so action methods so want to keep things as DRY as possible.

I haven't done much with custom model binders so I wonder if this is a job better suited for them. A key requirement is that I can inject my dependencies into the model binder (for ISiteContext and IRepository - I can fall back to DependencyResolver if necessary).

Many thanks,

Ben

Update

Below is the working code, using both a custom model binder and action filter. I'm still not sure how I feel about this because

  1. Should I be hitting my database from a modelbinder
  2. I can actually do both the retrieving of the object and null validation from within an action filter. Which is better?

Model Binder:

public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
    if (!controllerContext.RouteData.Values.ContainsKey("siteid"))
        return null;

    var siteId = controllerContext.RouteData.GetRequiredString("siteid");

    var site =
        siteRepository.Get(
            s => s.Account == siteContext.Account && s.SystemName == siteId
        );

    return site;
}

Action Filter:

public class ValidateSiteAttribute : ActionFilterAttribute
{       
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {           
        var site = filterContext.ActionParameters["site"];

        if (site == null || site.GetType() != typeof(Site))
            filterContext.Result = new HttpNotFoundResult();

        base.OnActionExecuting(filterContext);
    }
}

Controller Actions:

[HttpGet]
[ValidateSite]
public ActionResult Settings(Site site)
{
    var blog = site.GetFeature<BlogFeature>();
    var settings = settingsProvider.GetSettings<BlogSettings>(blog.Id);

    return View(settings);
}

[HttpPost]
[ValidateSite]
[UnitOfWork]
public ActionResult Settings(Site site, BlogSettings settings)
{
    if (ModelState.IsValid)
    {
        var blog = site.GetFeature<BlogFeature>();
        settingsProvider.SaveSettings(settings, blog.Id);
        return RedirectToAction("Settings");
    }

    return View(settings);
}
like image 950
Ben Foster Avatar asked Nov 04 '22 21:11

Ben Foster


2 Answers

This definitely sounds like a job for an action filter. You can do DI with action filters not a problem.

So yeah, just turn your existing functionality into a action filter and then apply that to each action OR controller OR a base controller that you inherit from.

I don't quite know how your site works but you could possibly use a global action filter that checks for the existence of a particular route value, e.g. 'SiteName'. If that route value exists, that means you need to follow through with checking that the site exists...

like image 172
Charlino Avatar answered Nov 09 '22 16:11

Charlino


A custom model binder for your Site type sounds like a good idea to me. You will probably also want an action filter as well to catch "null" and return not found.

like image 20
Andrew Davey Avatar answered Nov 09 '22 15:11

Andrew Davey