Route Parameter, Custom Model Binder or Action Filter? - asp.net-mvc

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:
Look for a site with the given identifier on the current account
If the site returned is null, return a 404 (or custom view)
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
Should I be hitting my database from a modelbinder
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);
}

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...

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.

Related

MVC3 using routes or using controller logic?

I'm relatively new with MVC3, but I'm using it, C# and EF4 to create an application website. The routing that I'm using is the same as in the default Microsoft project created when I selected MVC3 pattern, nothing special:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }, // Parameter defaults
new[] { "MySite.Controllers" }
);
}
And everything is working fine there. We're using the default Membership Provider, and users also get an INT value that identifies their account. This lets them see their profile pretty easily with a simple routing like:
www.mysite.com/profile/4
...for example. However, the client has asked that a lot of accounts be pre-generated and distributed to selected users. I've worked up a way to run that through SQL Server and it works fine, all the accounts got created (about a thousand). Additionally, I've add a bit field ('Claimed') that can help identify whether one of these pre-generated accounts has been 'activated' by these users.
My question is, when a user is given a link to come visit their (un-activated) account, should I use a test when doing the initial routing on that page to identify their account as un-claimed and send them somewhere else to finish entering details into their account? Or should I let them go to the same page as everyone else, and have something in the controller logic that identifies this record as un-claimed, and then send them to another page to finish entering details etc.? Is there a good reason for doing one over the other?
And what about people who make up (or have a typographical error) in their Id value, like:
www.mysite.com/profile/40000000000
(and the site only has a thousand users so far), should that be handled similarly, or through different means entirely? (I.e., in one scenario we're identifying an existing account that is not yet claimed, and in another scenario we're having to figure out that the account doesn't even exist.)
Any help would be greatly appreciated.
EDIT:
I'm trying to implement Soliah's suggested solution, and got stuck a bit on the fact that the if (id != 0) didn't like that the id might not be in an INT. I'm past that now, and attempting to figure out a way to do the check if valid portion, but possibly I have not solved the problem with the id not being treated as an INT? Something is definitely not right, even though I'm trying to convert it again during my database test for validity. Any ideas on why I'm getting the error below? What am I missing?
public class ValidProfileIdAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var id = (Convert.ToInt32(filterContext.ActionParameters["Id"]));
if (id != 0)
{
// Check if valid and behave accordingly here.
Profile profile = db.Profiles.Where(q => q.ProfileId == (Convert.ToInt32(id))).FirstOrDefault();
}
base.OnActionExecuting(filterContext);
}
}
Cannot implicitly convert type 'System.Linq.IQueryable<Mysite.Models.Profile>' to 'Mysite.Models.Profile'. An explicit conversion exists (are you missing a cast?)
EDIT #2:
I'm working on Robert's suggestion, and have made partial progress. My code currently looks like this:
public class UserAccountActivatedAttribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
bool isActivated = // some code to get this state
return isActivated;
}
}
which I got to after reading the blog entry, and (believe it or not) this posting: http://pastebin.com/Ea09Gf4B
I needed to change ActionSelectorAttribute to ActionMethodSelectorAttribute in order to get things moving again.
However, what I don't see how to do is to get the Id value into the bool isActivated test. My database has a view ('Claimed') which can give back a true/false value, depending on the user's profile Id that it is handed, but I don't see where to add the Id. Would something like what Soliah edited work?
if (int.TryParse(filterContext.ActionParameters["Id"], id) && id != 0) {
bool isActivated = db.Claimed.Where(c => c.ProfileId == id).FirstOrDefault();
EDIT #3:
Here is my current state of the code:
public class UserAccountActivatedAttribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
// get profile id first
int id = int.Parse((string)controllerContext.RouteData.Values["id"]);
var profile = db.Profiles.Where(q => q.ProfileId == id).FirstOrDefault();
bool isActivated = profile;// some code to get this state
return isActivated;
}
}
For me, I had to change things to int.Parse((string)controllerContext.RouteData.Values to get them to work, which they seem to do (to that point.) I discovered that formatting here: Bind a routevalue to a property of an object that is part of viewmodel
The line
var profile = db.Profiles.Where(q => q.ProfileId == id).FirstOrDefault();
errors on the db. section, with error message as follows:
Cannot access a non-static member of outer type 'MySite.Controllers.HomeController' via nested type 'MySite.Controllers.HomeController.UserAccountActivatedAttribute'
...which is something that I have diligently tried to figure out with MSDN and Stack, only to come up empty. Does this ring any bells?
Others have suggested many things already, but let me bring something else to the table here.
Action Method Selector
In order to keep your controller actions clean, you can write an action method selector attribute to create two simple actions:
[ActionName("Index")]
public ActionResult IndexNonActivated(int id)
{
...
}
[ActionName("Index")]
[UserAccountActivated]
public ActionResult IndexActivated(int id)
{
...
}
This way you don't deal with checking code in your actions keeping them really thin. Selector filter will make sure that correct action will get executed related to user account activation state.
You can read more about action selector attributes in my blog post but basically you'd have to write something similar to this:
public class UserAccountActivatedAttribute : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
// get profile id first
int id = int.Parse(controllerContext.RouteData.Values["id"] ?? -1);
bool isActivated = // some code to get this state
return isActivated;
}
}
And that's basically it.
This will make it possible for users to access their profile regardless whether their account has been activated or not. Or maybe even deactivated at some later time... And it will work seamlessly in the background.
One important advantage
If you'd have two actions with different names (as Juraj suggests), one for active profiles and other for activation, you'd have to do the checking in both, because even active users would be able to access activation action:
profile/4 > for active profiles
profile/activate/4 > for inactive profiles
Both actions should be checking state and redirect to each other in case that state doesn't "fit". This also means that each time a redirection would occur, profile will get checked twice. In each action.
Action method selector will check profiles only once. No matter what state user profile is in.
I'd prefer to keep my controller thin and place this in an action filter that you can annotate on the Index action of the Profile controller.
public class ValidProfileIdAttribute : ActionFilterAttribute {
public override void OnActionExecuting(ActinExecutingContext filterContext) {
int id;
if (int.TryParse(filterContext.ActionParameters["Id"], id) && id != 0) {
// Check if valid and behave accordingly here.
var profile = db.Profiles.Where(q => q.ProfileId == id).FirstOrDefault();
}
base.OnActionExecuting(filterContext);
}
}
The OnActionExecuting method will be called before your controller's action.
In your controller:
[ValidProfileId]
public ActionResult Index(int id) {
...
}
I would suggest to have that logic in the controller, as once he/she is activated, they may be able to use the same link to access their profile.
Checking whether an account is activated or not is a part of application logic and should be implemented inside the controller (or deeper). Within the controller, you can redirect un-activated users to any other controller/action to finish the activation. URL routing mechanism should route simply according to a shape of an incoming URL and shouldn't contact the database.

How to check record ownership in controllers?

An application permits users to create records. For our purposes, let's call those records Goals.
One user should not be able to see Goals created by another user.
What is the best method for preventing UserA from accessing UserB's Goal?
I can do it like this:
//using asp.net membership
Guid uId = (Guid)System.Web.Security.Membership.GetUser().ProviderUserKey;
//goal records contain a foreign key to users, so I know who owns what
Goal theGoal = db.Goals.SingleOrDefault(g => g.GoalId == goalId
&& g.UserId == uId);
if (null == theGoal)
{
ViewData["error"] = "Can't find that goal.";
return View("Error");
}
else
{
return View(theGoal);
}
This works fine. The problem is that I've got similar code littered in every action that accesses goals.
Is there a more re-usable way of accomplishing this?
I thought of implementing it as an Authorization Filter. 2 problems with that scheme:
1) Requires the filter to know about and use the DB.
2) Requires 2 queries(1 in the filter, another in the action) instead of just the 1 query in the action that I have now.
What's a more DRY way of accomplishing this?
A custom model binder is a great place to do this:
public class GoalModelBinder : DefaultModelBinder
{
private readonly IGoalRepository _repository;
public GoalModelBinder(IGoalRepository repository)
{
_repository = repository;
}
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
// Here the default model binder does his job of binding stuff
// like the goal id which you would use in the repository to check
var goal = base.CreateModel(controllerContext, bindingContext, modelType) as Goal;
var user = controllerContext.HttpContext.User;
var theGoal = _repository.GetGoal(user, goal);
if (theGoal == null)
{
throw new HttpException(403, "Not authorized");
}
// It's OK, we've checked that the Goal belongs to the user
// => return it
return theGoal;
}
}
and then in your Application_Start register this model binder:
// some implementation of your repo
var sqlRepo = new SqlGoalRepository();
ModelBinders.Binders.Add(typeof(Goal), new GoalModelBinder(sqlRepo));
Now your controller action becomes less littered:
[Authorize]
public ActionResult Edit(Goal goal)
{
// if we get that far we are fine => we've got our goal
// and we are sure that it belongs to the currently logged user
return View(goal);
}

OutputCache behavior in ASP.NET MVC 3

I was just testing Output Caching in the RC build of ASP.NET MVC 3.
Somehow, it is not honoring the VaryByParam property (or rather, I am not sure I understand what is going on):
public ActionResult View(UserViewCommand command) {
Here, UserViewCommand has a property called slug which is used to look up a User from the database.
This is my OutputCache declaration:
[HttpGet, OutputCache(Duration = 2000, VaryByParam = "None")]
However, when I try and hit the Action method using different 'slug' values (by manupulating the URL), instead of serving wrong data (which I am trying to force by design), it is instead invoking the action method.
So for example (in order of invocation)
/user/view/abc -> Invokes action method with slug = abc
/user/view/abc -> Action method not invoked
/user/view/xyz -> Invokes action method again with slug = xyz! Was it not supposed to come out of the cache because VaryByParam = none?
Also, what is the recommended way of OutputCaching in such a situation? (example above)
Just wanted to add this information so that people searching are helped:
The OutputCache behavior has been changed to be 'as expected' in the latest release (ASP.NET MVC 3 RC 2):
http://weblogs.asp.net/scottgu/archive/2010/12/10/announcing-asp-net-mvc-3-release-candidate-2.aspx
Way to go ASP.NET MVC team (and Master Gu)! You all are awesome!
VaryByParam only works when the values of the url look like /user/view?slug=abc. The params must be a QueryString parameter and not part of the url like your above examples. The reason for this is most likely because Caching happens before any url mapping and that mapping isn't included in the cache.
Update
The following code will get you where you want to go. It doesn't take into account stuff like Authorized filters or anything but it will cache based on controller/action/ids but if you set ignore="slug" it will ignore that particular attribute
public class ActionOutputCacheAttribute : ActionFilterAttribute {
public ActionOutputCacheAttribute(int cacheDuration, string ignore) {
this.cacheDuration = cacheDuration;
this.ignore = ignore;
}
private int cacheDuration;
private string cacheKey;
private string ignore;
public override void OnActionExecuting(ActionExecutingContext filterContext) {
string url = filterContext.HttpContext.Request.Url.PathAndQuery;
this.cacheKey = ComputeCacheKey(filterContext);
if (filterContext.HttpContext.Cache[this.cacheKey] != null) {
//Setting the result prevents the action itself to be executed
filterContext.Result =
(ActionResult)filterContext.HttpContext.Cache[this.cacheKey];
}
base.OnActionExecuting(filterContext);
}
public override void OnActionExecuted(ActionExecutedContext filterContext) {
//Add the ActionResult to cache
filterContext.HttpContext.Cache.Add(this.cacheKey, filterContext.Result,null, DateTime.Now.AddSeconds(cacheDuration),
System.Web.Caching.Cache.NoSlidingExpiration, CacheItemPriority.Normal, null);
//Add a value in order to know the last time it was cached.
filterContext.Controller.ViewData["CachedStamp"] = DateTime.Now;
base.OnActionExecuted(filterContext);
}
private string ComputeCacheKey(ActionExecutingContext filterContext) {
var keyBuilder = new StringBuilder();
keyBuilder.Append(filterContext.ActionDescriptor.ControllerDescriptor.ControllerName);
keyBuilder.Append(filterContext.ActionDescriptor.ActionName);
foreach (var pair in filterContext.RouteData.Values) {
if (pair.Key != ignore)
keyBuilder.AppendFormat("rd{0}_{1}_", pair.Key.GetHashCode(), pair.Value.GetHashCode());
}
return keyBuilder.ToString();
}
}

Find the atrributes on an action from the ViewEngine in ASP.NET MVC

I've got a custom ViewEngine and I want to modify the master page used depending on if the requested action has an Authorize attribute filter.
So far I'm just using reflection like this:
var method = controllerContext.Controller.GetType().GetMethod(viewName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase);
if (method != null)
{
if (method.GetCustomAttributes(typeof(AuthorizeAttribute), true).Length > 0)
{
masterName = "Admin.master";
}
}
But I'm not a huge fan of using reflection for repetitive tasks. I know I can use the view cache to speed things up after the first time, but I'm wondering if there is a more direct way to get access to the list of filters applied to the action inside the FindView method of the ViewEngine?
Your pretty much confined to using reflection to get any attribute information regardless for anything including MVC action methods. ;)
The only other technique to get this information is to go through the ControllerDescriptor pathways
In your case you could just skip looking for the authorize attribute at all and just asking if a user is authorized or not and giving them the master page they need.
I've set the master page dynamically in a custom view engine before and the most performant option is to look at whatever HttpContextBase information is available. For one scenario I just passed along query parameters or appending on {masterPage} route parameters when I need to use them.
The only other pathway to action information is the ReflectedControllerDescriptor route. The problem with this method is its very verbose and requires a lot of lines of code to do what your doing.
Here is a little bit of code ( I found on stackoverflow! ) of that "descriptor" technique to do security link pruning trimming. This code could also be used to dynamically set the masterpage if it was in a custom view engine. Its not what your looking for but may help somebody else accomplish the same dynamic masterpage setting someplace else:
public static bool HasActionPermission( this HtmlHelper htmlHelper, string actionName, string controllerName )
{
//if the controller name is empty the ASP.NET convention is:
//"we are linking to a different controller
ControllerBase controllerToLinkTo = string.IsNullOrEmpty(controllerName)
? htmlHelper.ViewContext.Controller
: GetControllerByName(htmlHelper, controllerName);
var controllerContext = new ControllerContext(htmlHelper.ViewContext.RequestContext, controllerToLinkTo);
var controllerDescriptor = new ReflectedControllerDescriptor(controllerToLinkTo.GetType());
var actionDescriptor = controllerDescriptor.FindAction(controllerContext, actionName);
return ActionIsAuthorized(controllerContext, actionDescriptor);
}
private static bool ActionIsAuthorized(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
{
if (actionDescriptor == null)
return false; // action does not exist so say yes - should we authorise this?!
AuthorizationContext authContext = new AuthorizationContext(controllerContext);
// run each auth filter until on fails
// performance could be improved by some caching
foreach (IAuthorizationFilter authFilter in actionDescriptor.GetFilters().AuthorizationFilters)
{
authFilter.OnAuthorization(authContext);
if (authContext.Result != null)
return false;
}
return true;
}

Modelbinding database entities in ASPNET MVC

I'm having trouble trying to think what the best way is to recreate a database object in a controller Action.
I want to make use of ModelBinders so in my action I have access to the object via a parameter, rather than having to repeat code to get an object from the database based on an identifier parameter. So I was thinking of having a ModelBinder that performs a call to the dataaccess layer to obtain the original object (or creates a new one if it doesn't exist in the database), then binds any properties to the database object to update it. However I've read that the ModelBinders shouldn't make database queries (first comment of this article).
If the ModelBinder shouldn't perform a database query (so just using the DefaultModelBinder) then what about database objects that have properties that are other db objects? These would never get assigned.
Saving an object after the user has edited it (1 or 2 properties are editable in the view) the ModelBinded object would be missing data, so saving it as it is would result in data in the database being overwritten with invalid values, or NOT-NULL constraints failing.
So, whats the best way to get an object in a controller action from the database bound with the form data posted back from the view?
Note im using NHibernate.
I get the model object from the database, then use UpdateModel (or TryUpdateModel) on the object to update values from the form parameters.
public ActionResult Update( int id )
{
DataContext dc = new DataContext();
MyModel model = dc.MyModels.Where( m => m.ID == id ).SingleOrDefault();
string[] whitelist = new string[] { "Name", "Property1", "Property2" };
if (!TryUpdateModel( model, whitelist )) {
... model error handling...
return View("Edit");
}
ViewData.Model = model;
return View("Show");
}
Unfortunately you don't have control over the construction of the model binder, so you can't inject any repository implementation.
You can reach out directly into a service locator to pull in your repository & fetch the item:
public class ProductBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext,
ModelBindingContext bindingContext, Type modelType)
{
if(modelType != typeof(Product))
return null;
var form = controllerContext.HttpContext.Request.Form;
int id = Int32.Parse(form["Id"]);
if(id == 0)
return base.CreateModel(controllerContext, bindingContext, modelType);
IProductRepository repository = ServiceLocator.Resolve<IProductRepository>();
return repository.Fetch(id);
}
}
You might even make this work for all of your entities if you can use a base class or interface that provides the Id of the class.
You'll have to set this up in Global.asax:
ModelBinders.Binders.Add(typeof(Product), new ProductBinder());
and then you can do this:
public ActionResult Save([Bind] Product product)
{
....
_repository.Save(product);
}
Let me first state that I don't recommend to access database from ModelBinders, as from perspective of Separation Of Concern ModelBinders should only be responsible of interpretting client request, obviously database is not.
If you dont want to repeat your self (DRY), use repositories/services
However if u really want to do it like that, then
In global.asax.cs Register a custom MyModelBinderProvider to MVC
ModelBinderProviders.BinderProviders.Add(new EntityModelBinderProvider
{
ConnectionString = "my connection string"
));
Cunstruct the custom ModelBinderProvider to contain database settings
public class EntityBinderProvider: IModelBinderProvider
{
public string ConnectionString { get; set; }
public IModelBinder GetBinder(Type modelType)
{
if (Is known entity)
return new EntityBinder(ConnectionString);
else
return null;
}
}
Follow further instructions from Ben Scheirman
You don't actually have to hit the database. Simply setting the Id of the objects will be enough to set the relationship up, but watch your cascades. Make sure your cascde settings won't update the related object as it will clear the values.

Resources