I have a problem with my custom RouteBase implementation and [OutputCache].
We have a CMS in which urls are mapped to certain content pages. Each type of content page is handled by a different controller (and different views). The urls are completely free and we want different controllers, so a "catchall" route is not usable. So we build a custom RouteBase implementation that calls the database to find all the urls. The database knows which Controller and Action to use (based on the content page type).
This works just great.
However, combining this with the [OutputCache] attribute, output caching doesn't work (the page still works). We made sure [OutputCache] works on our "normal" routes.
It is very difficult to debug outputcaching, the attribute is there we us it, it doesn't work... Ideas how to approach this would be very welcome, as would the right answer!
The controller looks like this:
public class TextPageController : BaseController
{
private readonly ITextPageController textPageController;
public TextPageController(ITextPageController textPageController)
{
this.textPageController = textPageController;
}
[OutputCache(Duration = 300)]
public ActionResult TextPage(string pageid)
{
var model = textPageController.GetPage(pageid);
return View(model);
}
}
The custom route looks like this:
public class CmsPageRoute : RouteBase
{
private IRouteService _routeService;
private Dictionary<string, RouteData> _urlsToRouteData;
public CmsPageRoute(IRouteService routeService)
{
this._routeService = routeService;
this.SetCmsRoutes();
}
public void SetCmsRoutes()
{
var urlsToRouteData = new Dictionary<string, RouteData>();
foreach (var route in this._routeService.GetRoutes()) // gets RouteData for CMS pages from database
{
urlsToRouteData.Add(route.Url, PrepareRouteData(route));
}
Interlocked.Exchange(ref _urlsToRouteData, urlsToRouteData);
}
public override RouteData GetRouteData(System.Web.HttpContextBase httpContext)
{
RouteData routeData;
if (_urlsToRouteData.TryGetValue(httpContext.Request.Path, out routeData))
return routeData;
else
return null;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return null;
}
private RouteData PrepareRouteData(ContentRouteData contentRoute)
{
var routeData = new RouteData(this, new MvcRouteHandler());
routeData.Values.Add("controller", contentRoute.Controller);
routeData.Values.Add("action", contentRoute.Action);
routeData.Values.Add("area", contentRoute.Area);
routeData.Values.Add("pageid", contentRoute.Constraints["pageid"]); // variable for identifying page id in controller method
routeData.DataTokens.Add("Namespaces", new[] { contentRoute.Namespace });
routeData.DataTokens.Add("area", contentRoute.Area);
return routeData;
}
// routes get periodically updated
public void UpdateRoutes()
{
SetCmsRoutes();
}
}
Thank you for reading until the end!
In the end we tracked it down to a call to
... data-role="#this.FirstVisit" ...
in our _Layout.cshtml
This called a property on our custom view page that in turn called a service which always set a cookie. (Yikes setting cookies in services!, we know!)
Had it not been Friday and at the end of the sprint we might have noticed the cache busting
Cache-Control: no-cache="Set-Cookie": Http Header.
I still don't understand why this only busted the cache for our custom RouteBase implementation and not all pages. All pages use the same _Layout.cshtml.
You can try the following code if it helps
[OutputCache(Duration = 300, VaryByParam="*")]
public ActionResult TextPage(string pageid)
{
var model = textPageController.GetPage(pageid);
return View(model);
}
You might look here to customize caching :
Ignore url values with Outputcache
Output Cache problem on ViewEngine that use 2 separate view for 1 controller
Also, check the cache location.
Related
If localization of the website is done in such a way that on every request in mvc controller I read current thread and upon that information partial view is loaded (cause every language has different partial view) and strings inside non partial view are localized using resource strings.
Controller using data annotation to set Localize custom attribute
[Localize]
public class HomeController : Controller
{
public ActionResult Index()
{
string partialView = string.Empty;
switch (Thread.CurrentThread.CurrentCulture.ToString())
{
case "en-US":
partialView = "english";
....
}
ViewBag.PartialViewName = partialView;
return View();
}
}
my question is: how can I cache this Index action knowing that there
is 7 different languages?
Update:
If I use for example
[OutputCache(Duration = 300)]
public ActionResult Index()
{}
then I'm losing switching language ability.
You could just cache by a custom parameter
[OutputCache(Duration = 3600, VaryByCustom = "CurCulture")]
In your projects global.asax.cs add the following:
public override string GetVaryByCustomString(HttpContext context, string arg)
{
if (arg.Equals("CurCulture"))
{
var culture = Thread.CurrentThread.CurrentUICulture;
return culture;
}
return base.GetVaryByCustomString(context, arg);
}
Now your outputcache is caching your views based upon the users current ui-culture.
If you have multiple VaryByCustom attributes across your controllers, you need to enhance that method with the aditional logic.
Reference: https://msdn.microsoft.com/en-us/library/5ecf4420.aspx
Use VaryByCustom property in OutputCache attribute
[OutputCache(VaryByCustom = "CultureCode")]
Also you can override GetVaryByCustomString(HttpContext context, string custom) in HttpApplication class(so in your Global.asax.cs file) and add logic that will return your current culture
I am new to web api coming from a WCF background and as prep I watched Shawn Wildermuth's Pluralsight course on the subject before diving in. His course material was designed around more traditional routing. One of the subjects the course dives into is HATEOAS and how easy it is to achieve this with a base api controller and model factory.
One of the first things I hit when implementing against attribute routing was the need for the UrlHelper to have a route name as the first argument of the Link() method, something that was inherited in the conventional routing configured in the WebApiConfig.cs.
I worked around this by decorating one of my controllers route attributes with the Name property and it appears that all methods in that controller have access to the name property regardless of which method I put it on (see code below). While I find this a bit odd, it works. Now that I had HATEOAS implemented, I noticed the URL's it was generating were in the query string format and not "url" formatted (I know the term is wrong but bear with me). Instead of .../api/deliverables/1 I am getting .../api/deliverables?id=1.
This is "ok" but not the desired output. While I still have not figured out how to adjust the formatting the of the return value of the URL, I figured I would test the query string against my controller and found that in the query string format my controller does not work but in the "url" format it does.
I then spent an hour trying to figure out why. I have attempted different decorations (i.e. [FromUri] which from my reading should only be necessary for complex objects which default to the message body) to setting default values, constraints and making it optional (i.e. {id?}).
Below is the code in question, both for the controller, the base api controller and the model factory that makes the HATEOAS implementation possible.
The 3 questions I have are:
1) How to make the controller accept the "id" on the querystring AND in the url format (.../deliverables/1 and .../deliverables?id=1.
2) How to make the Link method of the URL helper return the value in the url format (it is currently returning it as a query string.
3) Proper way to name routes in WebAPI 2. What I am doing (assigning a name to a single method and the others appear to inherit it simply smells and I have to believe this would crumble as I actually start to implement more complex code. Is Shawn's implementation flawed in some way? I like not having to hard code a URL for test/development purposes but maybe UrlHelper is not the best way to achieve this. It seems to carry with it a lot of baggage that may not be necessary.
Controller:
[RoutePrefix("api/deliverables")]
public class DeliverablesController : BaseApiController
{
private readonly IDeliverableService _deliverableService;
private readonly IUnitOfWork _unitOfWork;
public DeliverablesController(IDeliverableService deliverableService, IUnitOfWorkAsync unitOfWork)
{
_deliverableService = deliverableService;
_unitOfWork = unitOfWork;
}
[Route("", Name = "Deliverables")]
public IHttpActionResult Get()
{
return Ok(_deliverableService.Get().Select(TheModelFactory.Create));
}
[Route("{id}")]
public IHttpActionResult Get(int id)
{
return Ok(TheModelFactory.Create(_deliverableService.Find(id)));
}
[Route("")]
public IHttpActionResult Post([FromBody]DeliverableModel model)
{
try
{
var entity = TheModelFactory.Parse(model);
if (entity == null)
{
return BadRequest("Could not parse Deliverable entry in body.");
}
_deliverableService.Insert(entity);
_unitOfWork.SaveChanges();
return Created(Request.RequestUri + "/" + entity.Id.ToString(CultureInfo.InvariantCulture),TheModelFactory.Create(entity));
}
catch (Exception exception)
{
return BadRequest(exception.Message);
}
}
}
Base API Controller:
public abstract class BaseApiController : ApiController
{
private ModelFactory _modelFactory;
protected ModelFactory TheModelFactory
{
get
{
return _modelFactory ?? (_modelFactory = new ModelFactory(Request));
}
}
}
Model Factory:
public class ModelFactory
{
private readonly UrlHelper _urlHelper;
public ModelFactory(HttpRequestMessage request)
{
_urlHelper = new UrlHelper(request);
}
public DeliverableModel Create(Deliverable deliverable)
{
return new DeliverableModel
{
Url = _urlHelper.Link("deliverables", new { id = deliverable.Id }),
Description = deliverable.Description,
Name = deliverable.Name,
Id = deliverable.Id
};
}
public Deliverable Parse(DeliverableModel model)
{
try
{
if (string.IsNullOrEmpty(model.Name))
return null;
var entity = new Deliverable
{
Name = model.Name,
Description = !string.IsNullOrEmpty(model.Description)
? model.Description
: string.Empty
};
return entity;
}
catch (Exception)
{
return null;
}
}
}
As a point of clarification, non-attribute (traditional) routing works without an issue for both the URI and query string formats:
config.Routes.MapHttpRoute(
name: "deliverables",
routeTemplate: "api/deliverables/{id}",
defaults: new { controller = "deliverables", id = RouteParameter.Optional }
);
In my opinion, this is one of the problems with Attributed routing. That's why I use it for exceptional cases only. I use route tables for the majority of routing then drop down into attributed routing for exceptional cases.
To solve this your way, have you thought about multiple routes on the Get(id)? (I don't actually think this would work, but its worth a try).
I am relatively new to the MVC way of doing things and have run into a bit of a performance issue due to loading a lot of extraneous data in a base controller. I have read a bit about action filters and was wondering if anyone had advice on how to proceed. Here is my issue:
I have a Controller called RegController that inherits from a base Controller called BaseController. In the onActionExectuting method of the base controller I load several variables, etc. that will be used in a visible page such as viewbag, viewdata, etc. I only need to load this stuff for Actions with the result type ActionResult. In the same RegController I also have some JsonResult Actions that do not need all of this extra info loaded because they do not load a page or view template they only return Json. Does anyone have a suggestion how to handle this properly within existing framework features? Should I be putting these action in different controllers instead? I have many controllers with varying functionality split up in this same way and am hoping I can filter the action type versus moving them all to a JsonResultController or whatever.
Code (some of it pseudo) below:
public class BaseController : Controller
{
protected string RegId;
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
if (filterContext.ActionParameters.ContainsKey("rid") && filterContext.ActionParameters["rid"] != null)
RegId = filterContext.ActionParameters["rid"].ToString();
Reg oReg = new Reg(RegId);
// IF A VISIBLE PAGE IS BEING SERVED UP, I NEED TO LOAD THE VIEWDATA FOR THE PAGE
SetViewAndMiscData(oReg);
// IF THIS IS A JSON RESULT ACTION, I DO NOT WANT TO LOAD THE VIEWDATA FOR THE PAGE
}
}
public class RegController : BaseController
{
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
}
public ActionResult Page(string id)
{
// THE VIEW DATA SHOULD BE SET FROM THE BASECONTROLLER FOR USE IN THE PAGE
return View();
}
public JsonResult Save(string id, string data)
{
// I DON"T NEED ALL OF THE VIEW AND MISC DATA LOADED HERE, INCLUDING THE AUTO LOAD
// OF THE REG OBJECT SINCE I DO IT HERE FROM THE PASSED PARAMS.
GenericResponseObject Response = new GenericResponseObject();
Reg oReg = new Reg(id);
if (!oReg.IsValid)
{
Response.Status = 1;
Response.Message = "Invalid Record";
}
else
Response = oReg.SaveData(data);
return Json(Response);
}
}
I am trying to see if I can change Mvc routing to use a a ticket based system where the route is an guid string to an external dictionary of values. So I setup a new route like so:
routes.Add(new Route("{ticketid}", new Shared.TicketRouteHandler()));
And created a new route handler:
public class TicketRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new TicketHttpHandler(requestContext);
}
}
And a new HttpHandler class. In the ProcessRequest method, I read the values for the controller and action out of the ticket, and place them into the route data collection:
string ticketidString = this.Request.RouteData.GetRequiredString("ticketid");
var ticket = Shared.Ticket.GetTicket(ticketId);
foreach (var item in ticket)
{
Request.RouteData.Values[item.Key] = item.Value;
}
The controller is executed like normal from there.
In the controller I have it looking for a ticketid parameter and if not found it creates one, and does a RedirecToAction, populating the new Ticket with the controller / action values:
public ActionResult Index(Guid? ticketid)
{
var ticket = Shared.Ticket.GetOrCreateTicket(ticketid);
if (ticketid == null)
{
//push the new values into the ticket dictionary instead of the route
string controller = "Home";
string action = "Index";
ticket.SetRoute(controller, action);
return RedirectToAction(action, controller, new { ticketid = ticket.Id });
}
return View();
}
This is all working fine, the problem is the URL that is created from the redirect action looks like this:
http://localhost:49952/df3b9f26-6c1c-42eb-8d0d-178e03b7e6f6?action=Index&controller=Home
Why is ?action=Index&controller=Home getting attached to the url?
If I remove the extra pieces and refresh the page, everything loads perfectly from the ticket collection.
I imagine that it's happening after the HttpHandler code is finished.
What can I do to stop this behavior?
The RedirectToAction method makes a new request and changes the URL in the browser:
This part is changing the URL:
return RedirectToAction(action, controller, new { ticketid = ticket.Id });
1) Return View doesn't make a new requests, it just renders the view without changing URLs in the browser's address bar.
2) Return RedirectToAction makes a new requests and URL in the browser's address bar is updated with the generated URL by MVC.
3) Return Redirect also makes a new requests and URL in the browser's address bar is updated, but you have to specify the full URL to redirect
4) Between RedirectToAction and Redirect, best practice is to use RedirectToAction for anything dealing with your application actions/controllers. If you use Redirect and provide the URL, you'll need to modify those URLs manually when you change the route table.
5) RedirectToRoute redirects to the specifies route defined in the the Route table.
But, View doesn't, try to adapt your code to return a View instead! Or, in the worst case, make something to handle the URL from the Response!
After digging around the mvc4 source, I think I found a solution:
Derive a new type from RedirectToRouteResult, override ExecuteResult and modify the url generation to only return the ticket string:
public class TicketRedirectResult : RedirectToRouteResult
{
public override void ExecuteResult(ControllerContext context)
{
string destinationUrl = UrlHelper.GenerateUrl(
RouteName,
null /* actionName */,
null /* controllerName */,
//Only return the ticket id, not the entire dictionary
new RouteValueDictionary(new { ticketid = RouteValues["ticketid"] }),
Routes,
context.RequestContext, false /* includeImplicitMvcValues */);
// snip other code
}
}
Then in your controller override RedirectToAction to return an instance of the new derived type:
protected override RedirectToRouteResult RedirectToAction(string actionName, string controllerName, RouteValueDictionary routeValues)
{
RouteValueDictionary mergedRouteValues;
if (RouteData == null)
{
mergedRouteValues = MergeRouteValues(actionName, controllerName, null, routeValues, includeImplicitMvcValues: true);
}
else
{
mergedRouteValues = MergeRouteValues(actionName, controllerName, RouteData.Values, routeValues, includeImplicitMvcValues: true);
}
//Only change here
return new TicketRedirectResult(mergedRouteValues);
}
Now the redirects only populates the ticket portion in the url.
Maybe I should back-up and widen the scope before diving into the title question...
I'm currently writing a web app in ASP.NET MVC 1.0 (although I do have MVC 2.0 installed on my PC, so I'm not exactly restricted to 1.0) -- I've started with the standard MVC project which has your basic "Welcome to ASP.NET MVC" and shows both the [Home] tab and [About] tab in the upper-right corner. Pretty standard, right?
I've added 4 new Controller classes, let's call them "Astronomer", "Biologist", "Chemist", and "Physicist". Attached to each new controller class is the [Authorize] attribute.
For example, for the BiologistController.cs
[Authorize(Roles = "Biologist,Admin")]
public class BiologistController : Controller
{
public ActionResult Index() { return View(); }
}
These [Authorize] tags naturally limit which user can access different controllers depending on Roles, but I want to dynamically build a Menu at the top of my website in the Site.Master Page based on the Roles the user is a part of. So for example, if "JoeUser" was a member of Roles "Astronomer" and "Physicist", the navigation menu would say:
[Home] [Astronomer] [Physicist]
[About]
And naturally, it would not list links to "Biologist" or "Chemist" controller Index page.
Or if "JohnAdmin" was a member of Role "Admin", links to all 4 controllers would show up in the navigation bar.
Ok, you prolly get the idea... Now for the real question...
Starting with the answer from this StackOverflow topic about Dynamic Menu building in ASP.NET, I'm trying to understand how I would fully implement this. (I'm a newbie and need a little more guidance, so please bare with me.)
The answer proposes Extending the Controller class (call it "ExtController") and then have each new WhateverController inherit from ExtController.
My conclusion is that I would need to use Reflection in this ExtController Constructor to determine which Classes and Methods have [Authorize] attributes attached to them to determine the Roles. Then using a Static Dictionary, store the Roles and Controllers/Methods in key-value pairs.
I imagine it something like this:
public class ExtController : Controller
{
protected static Dictionary<Type,List<string>> ControllerRolesDictionary;
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
// build list of menu items based on user's permissions, and add it to ViewData
IEnumerable<MenuItem> menu = BuildMenu();
ViewData["Menu"] = menu;
}
private IEnumerable<MenuItem> BuildMenu()
{
// Code to build a menu
SomeRoleProvider rp = new SomeRoleProvider();
foreach (var role in rp.GetRolesForUser(HttpContext.User.Identity.Name))
{
}
}
public ExtController()
{
// Use this.GetType() to determine if this Controller is already in the Dictionary
if (!ControllerRolesDictionary.ContainsKey(this.GetType()))
{
// If not, use Reflection to add List of Roles to Dictionary
// associating with Controller
}
}
}
Is this doable? If so, how do I perform Reflection in the ExtController constructor to discover the [Authorize] attribute and related Roles (if any)
ALSO! Feel free to go out-of-scope on this question and suggest an alternate way of solving this "Dynamic Site.Master Menu based on Roles" problem. I'm the first to admit that this may not be the best approach.
EDIT
After much reading and experimenting, I came up with my own solution. See below for my answer. Any constructive feedback / criticism welcome!
I prefer linking to everything in my Menus and creating a HtmlHelper which checks to see if a link is accessible or not based on the [Authorize] attributes.
Ok, so I decided to flesh out my own Extended Controller class like I originally proposed. Here is a very basic version. I can see various ways of making this better (extending further, tightening up the code, etc.) but I thought I would offer up my basic results because I imagine there are plenty of other people that want something similar, but might not want all the extras.
public abstract class ExtController : Controller
{
protected static Dictionary<string, List<string>> RolesControllerDictionary;
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
// build list of menu items based on user's permissions, and add it to ViewData
IEnumerable<MenuItem> menu = BuildMenu();
ViewData["Menu"] = menu;
}
private IEnumerable<MenuItem> BuildMenu()
{
// Code to build a menu
var dynamicMenu = new List<MenuItem>();
SomeRoleProvider rp = new SomeRoleProvider();
// ^^^^^INSERT DESIRED ROLE PROVIDER HERE^^^^^
rp.Initialize("", new NameValueCollection());
try
{ // Get all roles for user from RoleProvider
foreach (var role in rp.GetRolesForUser(HttpContext.User.Identity.Name))
{ // Check if role is in dictionary
if (RolesControllerDictionary.Keys.Contains(role))
{
var controllerList = RolesControllerDictionary[role];
foreach (var controller in controllerList)
{ // Add controller to menu only if it is not already added
if (dynamicMenu.Any(x => x.Text == controller))
{ continue; }
else
{ dynamicMenu.Add(new MenuItem(controller)); }
}
}
}
}
catch { } // Most role providers can throw exceptions. Insert Log4NET or equiv here.
return dynamicMenu;
}
public ExtController()
{
// Check if ControllerRolesDictionary is non-existant
if (RolesControllerDictionary == null)
{
RolesControllerDictionary = new Dictionary<string, List<string>>();
// If so, use Reflection to add List of all Roles associated with Controllers
const bool allInherited = true;
const string CONTROLLER = "Controller";
var myAssembly = System.Reflection.Assembly.GetExecutingAssembly();
// get List of all Controllers with [Authorize] attribute
var controllerList = from type in myAssembly.GetTypes()
where type.Name.Contains(CONTROLLER)
where !type.IsAbstract
let attribs = type.GetCustomAttributes(allInherited)
where attribs.Any(x => x.GetType().Equals(typeof(AuthorizeAttribute)))
select type;
// Loop over all controllers
foreach (var controller in controllerList)
{ // Find first instance of [Authorize] attribute
var attrib = controller.GetCustomAttributes(allInherited).First(x => x.GetType().Equals(typeof(AuthorizeAttribute))) as AuthorizeAttribute;
foreach (var role in attrib.Roles.Split(',').AsEnumerable())
{ // If there are Roles associated with [Authorize] iterate over them
if (!RolesControllerDictionary.ContainsKey(role))
{ RolesControllerDictionary[role] = new List<string>(); }
// Add controller to List of controllers associated with role (removing "controller" from name)
RolesControllerDictionary[role].Add(controller.Name.Replace(CONTROLLER,""));
}
}
}
}
}
To use, just:
Add the [Authorize(Roles="SomeRole1,SomeRole2,SomeRole3,etc."] to the Controller Class
Replace the inherited "Controller" with "ExtController".
For example:
[Authorize(Roles = "Biologist,Admin")]
public class BiologistController : ExtController
{
public ActionResult Index()
{ return View(); }
}
If you don't replace "Controller" with "ExtController", then that Controller won't have a dynamic menu. (This could be useful, in some scenarios, I think...)
In my Site.Master file, I changed the "menu" section to look like this:
<ul id="menu">
<li><%= Html.ActionLink("Home", "Index", "Home")%></li>
<% if (ViewData.Keys.Contains("Menu"))
{
foreach (MenuItem menu in (IEnumerable<MenuItem>)ViewData["Menu"])
{ %>
<li><%= Html.ActionLink(menu.Text, "Index", menu.Text)%></li>
<% }
}
%>
<li><%= Html.ActionLink("About", "About", "Home")%></li>
</ul>
And that's it! :-)
I met the same problem that requires the logic to stay in controller side. But I do like John's approach as it uses the system filter to decide if an action is authorized. In case it helps anyone, the following code removed the HtmlHelper from John's approach:
protected bool HasActionPermission(string actionName, string controllerName)
{
if (string.IsNullOrWhiteSpace(controllerName))
return false;
var controller = GetControllerByName(ControllerContext.RequestContext, controllerName);
var controllerDescriptor = new ReflectedControllerDescriptor(controller.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, actionDescriptor);
// run each auth filter until on fails
// performance could be improved by some caching
foreach (var filter in FilterProviders.Providers.GetFilters(controllerContext, actionDescriptor))
{
var authFilter = filter.Instance as IAuthorizationFilter;
if (authFilter == null)
continue;
authFilter.OnAuthorization(authContext);
if (authContext.Result != null)
return false;
}
return true;
}
private static ControllerBase GetControllerByName(RequestContext context, string controllerName)
{
IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory();
IController controller = factory.CreateController(context, controllerName);
if (controller == null)
{
throw new InvalidOperationException(
String.Format(
CultureInfo.CurrentUICulture,
"Controller factory {0} controller {1} returned null",
factory.GetType(),
controllerName));
}
return (ControllerBase)controller;
}