MVC3 using routes or using controller logic? - asp.net-mvc

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.

Related

How to count number of hits to the website using MVC [duplicate]

what is the best way to capture page views by person without slowing down performance on the site. I see that stackoverflow show page views all over the place. Are they doing an insert into a db everytime i click on a page?
In asp.net-mvc, Is there any recommended way to track page view per user (my site has a login screen) so i can review which pages people are going to and how often
First off.. if what you really care about is how are customers using my site then you most likely want to look into Google Analytics or a similar service.
But if you want a quick and dirty page view record and you are using ASP.Net MVC 3 then as Chris Fulstow mentioned you're going to want to use a mix of global action filters and caching. Here is an example.
PageViewAttribute.cs
public class PageViewAttribute : ActionFilterAttribute
{
private static readonly TimeSpan pageViewDumpToDatabaseTimeSpan = new TimeSpan(0, 0, 10);
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var calledMethod = string.Format("{0} -> {1}",
filterContext.ActionDescriptor.ControllerDescriptor.ControllerName,
filterContext.ActionDescriptor.ActionName);
var cacheKey = string.Format("PV-{0}", calledMethod);
var cachedResult = HttpRuntime.Cache[cacheKey];
if(cachedResult == null)
{
HttpRuntime.Cache.Insert(cacheKey, new PageViewValue(), null, DateTime.Now.Add(pageViewDumpToDatabaseTimeSpan) , Cache.NoSlidingExpiration, CacheItemPriority.Default,
onRemove);
}
else
{
var currentValue = (PageViewValue) cachedResult;
currentValue.Value++;
}
}
private static void onRemove(string key, object value, CacheItemRemovedReason reason)
{
if (!key.StartsWith("PV-"))
{
return;
}
// write out the value to the database
}
// Used to get around weird cache behavior with value types
public class PageViewValue
{
public PageViewValue()
{
Value = 1;
}
public int Value { get; set; }
}
}
And in your Global.asax.cs
public class MvcApplication : HttpApplication
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new PageViewAttribute());
}
}
For pre-ASP.Net MVC 3 ONLY you are going to have to apply the same attribute manually to all of your actions.
[PageView]
public ActionResult CallOne()
{
}
[PageView]
public ActionResult CallTwo()
{
}
The best way would probably be a global action filter that intercepts requests to all actions on all controllers, then increments a counter in the database for the current user and page. To save hitting the database too hard, you could cache these values and invalidate them every few minutes, depending on how much traffic you're dealing with.
We use the open source Piwik: http://piwik.org/, which is setup on it's own server. One line of Javascript in the _Layout page makes a call to Piwik after the page has loaded (put the JS at the end) and does not affect page load performance at all.
In addition to just counts, you'll get a ton of info about where your users are coming from, browser, screen resolutions, installed plugins. Plus you can track conversions and use the same tool to track marketing campaigns, etc.
<soapbox>
I cannot think of a situation where you'd be better off implementing this in MVC or in your web app in general. This stuff simply does not belong in your web app and is a meta-concern that should be separated out. This approach has enabled us to track analytics for all of our apps (32 of them: mvc 2/3, webforms, php...) in a unified manner.
If you really don't want to use another tool for this purpose, I would recommend tapping into your IIS log and getting your stats from there. Again, to get any real decision making power out of it, you'll need to put a good analyzer on it. I recommend Splunk: http://www.splunk.com/
</soapbox>
I wanted to post an updated version of Shane's answer for those who are interested. Some things to consider:
You have to set the action attribute up as a service when decorating your
methods using syntax like the following :
[ServiceFilter(typeof(PageViewAttribute))]
As far as I can tell, HttpRuntime.Cache.Insert isn't a thing in .NET Core, so I used a simple implementation of IMemoryCache (You may need to add this line to your startup.cs in order to use the interface):
services.AddMemoryCache();
Because we are injecting IMemoryCache into a class that is not a controller, we need to register our attribute as a service in startup.cs, like so:
services.AddScoped<[PageViewAttribute]>(); - without brackets!
Whatever object you return when creating a cacheKey will be assigned to the 'value' parameter of the OnRemove method.
Below is the code.
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var controllerActionDescriptor = filterContext.ActionDescriptor as ControllerActionDescriptor;
var arguments = filterContext.ActionArguments;
ActionId = arguments["id"].ToString();
var calledMethod = string.Format("{0} -> {1}",
controllerActionDescriptor.ControllerName,
controllerActionDescriptor.ActionName);
var cacheKey = string.Format("PV-{0}", calledMethod);
var cachedResult = _memoryCache.Get(cacheKey);
if (cachedResult == null)
{
//Get cacheKey if found, if not create cache key with following settings
_memoryCache.GetOrCreate(cacheKey, cacheKey =>
{
cacheKey.AbsoluteExpirationRelativeToNow
= pageViewDumpToDatabaseTimeSpan;
cacheKey.SetValue(1);
cacheKey.RegisterPostEvictionCallback(onRemove);
return cacheKey.Value;
});
}
else
{
_memoryCache.Get(cacheKey);
}
}
//Called when Memory entry is removed
private void onRemove(object key, object value, EvictionReason reason, object state)
{
if (!key.ToString().StartsWith("PV-"))
{
return;
}
// write out the value to the database
SaveToDataBase(key.ToString(), (int)value);
}
As a point of reference, this was done for a .NET Core 5 MVC App.
Regards.

Multiple views modifying one object MVC

I am building a service which requires a somewhat lengthy setup process. I have it broken into 4 models and 4 corresponding views. They are Setup, Setup2, Setup3, and Setup4. Each of these views gathers information from the user which is stored in a User object. I have been passing the user along like this:
[HttpPost]
public ActionResult Setup(FormCollection values)
{
User registeringUser = new User();
registeringUser.email = User.Identity.Name;
registeringUser.fName = values["fName"];
registeringUser.lName = values["lName"];
registeringUser.phone = values["phone"];
return RedirectToAction("/Setup2", registeringUser);
}
For some reason, this seems to work just fine for the first jump (from Setup to Setup2) but after that I'm getting weird behavior, such as User. getting set to null when the User is passed to another View.
In a related, but slightly different issue, I need the last screen (Setup4) to be recursive. This screen adds a course in which the user is enrolled, and if they don't check the "This was my last class" button, it needs to basically clear the form so they can enter another course.
The entire Controller looks like this:
[HttpPost]
public ActionResult Setup4(FormCollection values, User registeringUser)
{
// values["allClassesAdded"] returns "false" as a string if box is unchecked, returns "true,false" if checked.
// Solution: parse string for "true"
if (utils.parseForTrue(values["allClassesAdded"]))
{
// TODO Redirect to "congratulations you're done" page.
database.CreateUserInDB(registeringUser);
return Redirect("/Home");
}
else
{
// Build course and add it to the list in the User
course c = new course(values);
if (Request.IsAuthenticated)
{
//registeringUser.currentCourses.Add(c);
registeringUser.AddCourse(c);
return RedirectToAction("/Setup4", registeringUser); // <---- This doesn't really work right
//return View();
}
else
{
return Redirect("/Account/Login");
}
}
}
This is my first project with MVC, so if you find that I'm doing the entire thing completely incorrectly, feel free to not answer the question I asked and offer the proper solution to this need. I'm moving an existing (pure) C# project to MVC and I'm mainly just stuck on how to work within MVC's interesting structure. I'm very grateful for any help you can give!
Thanks!
You can store user related data in session without passing it between requests
Smth like this
[HttpPost]
public ActionResult Step1(Step1Model model)
{
Session["UserRegistration"] = new UserRegistration
{
FirstName = model.fName,
....
}
....
}
[HttpPost]
public ActionResult Step2(Step2Model model)
{
var userRegistration = Session["UserRegistration"] as UserRegistration;
if (userRegistration == null) { return Redirrect("Step1"); }
userRegistration.SomeField = model.someField;
...
Session["UserRegistration"] = userRegistration;
....
}

Derived from AuthorizeAttribute but User.Identity.Name is null unless using AuthorizeAttribute

So I've created a custom authorize attribute I use in a few places that is derived from an abstract base class which is derived from AuthorizeAttribute:
CustomAuthorizeAttributeBase.cs
public abstract class CustomAuthorizeAttributeBase : AuthorizeAttribute
{
public abstract string GetUsers();
public abstract string GetRoles();
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.IsChildAction)
{
return;
}
filterContext.Result =
new RedirectToRouteResult(new RouteValueDictionary
{
{"controller", "NotAuthorized"},
{"action", "Index"},
});
}
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (GetUsers().IndexOf(httpContext.User.Identity.Name, StringComparison.CurrentCultureIgnoreCase) >= 0 ||
GetRoles().Split(',').Any(s => httpContext.User.IsInRole(s)))
{
return true;
}
return false;
}
}
AreaLevelReadonly.cs
public class AreaLevelReadOnly : CustomAuthorizeAttributeBase
{
public override string GetUsers()
{
return ConfigurationManager.AppSettings["AreaReadonlyUsers"];
}
public override string GetRoles()
{
return ConfigurationManager.AppSettings["AreaReadonlyRoles"];
}
}
I also have some fairly simple code that gets me the currently logged in user:
UserIdentity.cs
public class UserIdentity : IUserIdentity
{
public string GetUserName()
{
return HttpContext.Current.User.Identity.Name.Split('\\')[1];
}
}
However, when I add my AreaLevelReadonly attribute to my controllers, getUserName fails and returns an exception that Name is null. I agonized over it for about an hour before putting authorize attribute on there as well, at which point it magically started working again. So, what is so different on the implementation level that my attribute deriving from authorizeattribute doesn't cause the Name to be populated.
Note: Windows authentication is on for the area, and the code works, but I don't understand why the Readonly attribute isn't enough to trigger authorization and population of the HttpContext.Current.User.Identity.Name.
Edit: Working:
[AreaLevelReadonly]
[Authorize]
public class DeleteAreaDataController : Controller {
//etc
var username = _userIdentity.GetUserName(HttpContext);
//etc
}
Exception on name:
[AreaLevelReadonly]
public class DeleteAreaDataController : Controller {
//etc
var username = _userIdentity.GetUserName(HttpContext);
//etc
}
More likely than not, you're accessing User.Identity.Name before it's populated. By including the standard Authorize attribute, as well, your code is then only running after the user has been authorized already and User.Identity.Name has been populated.
EDIT
Sorry, I misunderstood where the code attempting to call User.Identity.Name was running. Based on the belief that it was happening in your custom attribute, I was suggesting that you're trying to access it too early. However, I now see that you're calling it in your controller (although an explanation of what happens in GetUserAccount(HttpContext) would have helped.)
Anyways, your custom attribute obviously adds extra conditions on whether a user is authorized or not. When you return false, there is no user. It's not a situation where the user is "logged in" but not allowed to see the page. It's either there or it isn't. So the user is failing authorization based on your custom attribute (User.Identity.Name is null) but is authorized when you include Authorize (User.Identity.Name has a value).
Long and short, your GetUserName or GetUserAccount or whatever code needs to account for when the user has failed authorization. Or, if the user shouldn't be failing authorization, you'll need to look into why your custom attribute isn't working. Though, either way, you should still account for User.Identity.Name being null.
Your custom attribute is probably reading User.Identity.Name before you check that the user is authenticated.
In other words, in IsAuthorized(), before you read User.Identity.Name, you should be doing something like this:
if (!user.Identity.IsAuthenticated)
{
// Your custom code...
return false;
}
The reason you need this is because Windows Authentication (at least for NTLM) is a 2-step negotiation process between the client and server (see https://support.citrix.com/article/CTX221693). There will be 2 requests - the first with no name, and the second with a name. You can test this yourself - the source code for AuthorizeAttribute is provided here. Copy/paste that into your code and put a breakpoint in IsAuthorized - you will see that the breakpoint is hit twice. First time, the name is null, second time, it's set to your username.
So I think the solution is to either check user.Identity.IsAuthenticated at the start of your method, if you need to run custom code (as shown above), or alternatively if you only need to return false, simply replace the above code with base.IsAuthorized() which should do it for you.

Manipulate the url using routing

In my website I have the following route defined:
routes.MapRoute(
name: "Specific Product",
url: "product/{id}",
defaults: new { controller = "", action = "Index", id = UrlParameter.Optional }
);
In that way I want customers to be able to add the ID of the product and go to the product page.
SEO advisors have said that it would be better if we could add a description of the product on the URL, like product-name or something. So the URL should look something like:
/product/my-cool-product-name/123
or
/product/my-cool-product-name-123
Of course the description is stored in the db and I cannot do that with a url rewrite (or can I?)
Should I add a redirection on my controller (this would seem to do the job, but it just doesn't feel right)
On a few sites I checked they do respond with a 301 Moved Permanently. Is that really the best approach?
UPDATE
As per Stephen Muecke's comment I checked on what is happening on SO.
The suggested url was my own Manipulate the url using routing and i opened the console to see any redirections. Here is a screenshot:
So, first of all very special thanks to #StephenMuecke for giving the hint for slugs and also the url he suggested.
I would like to post my approach which is a mix of that url and several other articles.
My goal was to be able to have the user enter a url like:
/product/123
and when the page loads to show in the address bar something like:
/product/my-awsome-product-name-123
I checked several web sites that have this behaviour and it seems that a 301 Moved Permanently response is used in all i checked. Even SO as shown in my question uses 301 to add the title of the question. I thought that there would be a different approach that would not need the second round trip....
So the total solution i used in this case was:
I created a SlugRouteHandler class which looks like:
public class SlugRouteHandler : MvcRouteHandler
{
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
var url = requestContext.HttpContext.Request.Path.TrimStart('/');
if (!string.IsNullOrEmpty(url))
{
var slug = (string)requestContext.RouteData.Values["slug"];
int id;
//i care to transform only the urls that have a plain product id. If anything else is in the url i do not mind, it looks ok....
if (Int32.TryParse(slug, out id))
{
//get the product from the db to get the description
var product = dc.Products.Where(x => x.ID == id).FirstOrDefault();
//if the product exists then proceed with the transformation.
//if it does not exist then we could addd proper handling for 404 response here.
if (product != null)
{
//get the description of the product
//SEOFriendly is an extension i have to remove special characters, replace spaces with dashes, turn capital case to lower and a whole bunch of transformations the SEO audit has requested
var description = String.Concat(product.name, "-", id).SEOFriendly();
//transform the url
var newUrl = String.Concat("/product/",description);
return new RedirectHandler(newUrl);
}
}
}
return base.GetHttpHandler(requestContext);
}
}
From the above i need to also create a RedirectHandler class to handle the redirections. This is actually a direct copy from here
public class RedirectHandler : IHttpHandler
{
private string newUrl;
public RedirectHandler(string newUrl)
{
this.newUrl = newUrl;
}
public bool IsReusable
{
get { return true; }
}
public void ProcessRequest(HttpContext httpContext)
{
httpContext.Response.Status = "301 Moved Permanently";
httpContext.Response.StatusCode = 301;
httpContext.Response.AppendHeader("Location", newUrl);
return;
}
}
With this 2 classes i can transform product ids to SEO friendly urls.
In order to use these i need to modify my route to use the SlugRouteHandler class, which leads to :
Call SlugRouteHandler class from the route
routes.MapRoute(
name: "Specific Product",
url: "product/{slug}",
defaults: new { controller = "Product", action = "Index" }
).RouteHandler = new SlugRouteHandler();
Here comes the use of the link #StephenMuecke mentioned in his comment.
We need to find a way to map the new SEO friendly url to our actual controller. My controller accepts an integer id but the url will provide a string.
We need to create an Action filter to handle the new param passed before calling the controller
public class SlugToIdAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var slug = filterContext.RouteData.Values["slug"] as string;
if (slug != null)
{
//my transformed url will always end in '-1234' so i split the param on '-' and get the last portion of it. That is my id.
//if an id is not supplied, meaning the param is not ending in a number i will just continue and let something else handle the error
int id;
Int32.TryParse(slug.Split('-').Last(), out id);
if (id != 0)
{
//the controller expects an id and here we will provide it
filterContext.ActionParameters["id"] = id;
}
}
base.OnActionExecuting(filterContext);
}
}
Now what happens is that the controller will be able to accept a non numeric id which ends in a number and provide its view without modifying the content of the controller. We will only need to add the filter attribute on the controller as shown in the next step.
I really do not care if the product name is actually the product name. You could try fetching the following urls:
\product\123
\product\product-name-123
\product\another-product-123
\product\john-doe-123
and you would still get the product with id 123, though the urls are different.
Next step is to let the controller know that it has to use a special filer
[SlugToId]
public ActionResult Index(int id)
{
}

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

Resources