I'm creating a CMS using ASP.NET MVC, and by design, I've decided that each plugin (add-on) should have a key in the incoming HTTP request. Thus, I have this general route in my host application:
{pluginKey}/{controller}/{action}/{id}
I've created a custom controller factory which implements IControllerFactory and of course, it has a method to create controllers base on the ReqeustContext and controller name. However, I want to create an artificial HttpContext (alongside all other relevant objects like HttpRequest, RequestContext, RouteData, etc.) so that controllers of plugins won't misinterpret these URL segments wrongly. In other words, I want to cut the first part of the incoming URL, and make plugins think that they're processing this URL:
{controller}/{action}/{id}
How can I achieve this?
While you could create a new implementation of all the context classes, it seems like a bit of overkill. Why not use a derived Route Handler that applies the filtering functionality before returning the HttpHandler? Here's an example:
// To avoid conflicts with similarly named controllers, I find it to be good practice
// to create a route constraint with the set of all plugin names. If you don't have
// this function already, you should be able to access it with reflection (one time
// per app lifecycle) or you hard-code them. The point is to have a regex which ensures
// only valid plugins will get selected
string[] pluginNames = GetPluginNames();
string pluginNameRegex = string.Join("|",pluginNames);
Route pluginRoute = new Route (
url: "{pluginKey}/{controller}/{action}/{id}",
defaults: null,
constraints: new RouteValueDictionary(new { pluginKey = pluginNameRegex }),
routeHandler: new PluginRouteHandler()
});
// The custom route handler can modify your route data after receiving the RequestContext
// and then send it to the appropriate location. Here's an example (markdown code/untested)
// Note: You don't have to inherit from MvcRouteHandler (you could just implement IRouteHandler
// but I'm assuming you want Mvc functionality as the fallback)
public class PluginRouteHandler : MvcRouteHandler
{
public PluginRouteHandler(IControllerFactory controllerFactory)
: base(controllerFactory)
{}
protected override IHttpHandler GetHttpHandler(RequestContext requestContext){
if(ValidatePluginRoute(requestContext))
{
// we are going to remove the pluginKey from the RequestContext, It's probably wise
// to go ahead and add it to HttpContext.Items, in case you need the data later
requestContext.HttpContext.Items["pluginKey"] = requestContext.RouteData.Values["pluginKey"];
// now let's get ride of it, so your controller factory will process the
// requestContext as you have described.
requestContext.Values.Remove("pluginKey");
// the route will now be interpreted as described so let the flow go to the MvcRouteHandler's method
}
return base.GetHttpHandler(requestContext);
}
static bool ValidatePluginRoute(RequestContext requestContext){
return requestContext.RouteData.ContainsKey("pluginKey");
}
}
Related
I am building a web service using ServiceStack which has to support multiple vendors. The web service provides largely the same functionality to all vendors with some exceptions here and there.
In order to re-use as much functionality as possible I have come up with the following URL scheme:
http://localhost/brand1/templates
http://localhost/brand2/templates
"brand1" and "brand2" are not services but "templates" is. The Templates service's request DTO's will have a property called "Brand" like so:
[Route("/{Brand}/templates", "GET")]
public class GetTemplates
{
public Brand Brand { get; set; }
}
So in the Templates service I know which brand I am dealing with. This scheme works well.
What I cannot figure out though is this. The user of the service has to be authenticated and I cannot figure out how to handle the redirection of the service after the user has been authenticated since I have to pass along the brand information. I have created my own CustomAuthProvider class that inherits CredentialsAuthProvider. In the TryAuthenticate method I can set the authService.GetSession().ReferrerUrl property to the correct brand if I know what it was.
The only way I have found so far to get this information is to register a PreRequestFilter. My thinking here was that since the URL (e.g. http://localhost/brand1/templates) contains the brand I can store it in my own AuthUserSession class. I can't figure out how to do this. I have a "SessionFactory" method that I pass to the AuthFeature constructor. But what should I do in there? How do I get to the brand that I've obtained in the PreRequestFilter? Is it safe to store it in a field of the AppHost? I think not because of concurrency issues. How do I tie the PreRequestFilter to the SessionFactory method?
I hope I am explaining my problem clearly enough?
I was overthinking the solution because I didn't realize that I had all the information I needed in the IServiceBase parameter of the TryAuthenticate method of the CredentialsAuthProvider class.
In the end I came to the following solution:
public class CustomCredentialsAuthProvider : CredentialsAuthProvider
{
public override bool TryAuthenticate(IServiceBase authService,
string userName, string password)
{
var session = authService.GetSession();
var origQuery = authService.Request.UrlReferrer.Query;
session.ReferrerUrl = "/error";
var queryString = origQuery.Substring(10); // strip "redirect="
var decodedUrl = HttpUtility.UrlDecode(queryString);
if (!string.IsNullOrWhiteSpace(decodedUrl))
{
var query = new Uri(decodedUrl);
session.ReferrerUrl = query.AbsolutePath;
}
return DoAuthentication(userName, password);
}
}
The different places where you can set the Url to redirect to during ServiceStack Authentication, in order of precedence are:
The Session.ReferrerUrl Url if it's populated
The Continue QueryString, FormData param when making the request to /auth (i.e Authenticate.Continue property)
The HTTP Referer HTTP Header
The CallbackUrl of the current AuthProvider used
I'm using orchardcms 1.9 (no tag created jet). I am writing a custom module that implements its own controller that calles a service wich check some information and based on the service response I either redirect or let the user stay on the page.
The module is on the default layer in other words it is on everypage. So when user tries to log in or register this module checks information normally.
This is my route:
new RouteDescriptor {
Priority = -1,
Route = new Route(
"{*path}", // this is the name of the page url
new RouteValueDictionary {
{"area", "modulename"}, // this is the name of your module
{"controller", "controllername"},
{"action", "Redirect"}
},
new RouteValueDictionary(),
new RouteValueDictionary {
{"area", "modulename"} // this is the name of your module
},
new MvcRouteHandler())
and this is my controller:
public ActionResult Redirect()
{
String response = _authService.VerifyRegistration(_orchardServices.WorkContext.CurrentUser);
if (response.Equals("2"))
{
Response.Redirect("~/Registration");
}
else if (response.Equals("3"))
{
Response.Redirect("~/Users/Account/LogOn");
}
return View();
}
What happens is that when I go to registration or login controller triggers, checks the infromation, says no redirect needed then returns view. But because the view is empty my page is blank instead of its default login/registration form.
How can I solve this? Am I making a mistake in routing that I somehow override the default view (I tried different priorities but same response).
Your route overrides all routes ({*path}. So when you redirect, you redirect to....your redirector I guess. Therefore the view you are rendering is the one for your controller, not the page you were after.
Whatever the logic flaw - this is not a good way to globally control authorization type scenarios on your site. If you meant to have a single page that people might go to (e.g. http://www.mysite/welcome) then your problem is that your route is too global. However, if, as your code suggests that you want to create a "all pages" check to see if you should go to login or register, then you should implement an authorization filter. An example of an authorization filter (for a slightly different purpose) can be found at https://stackoverflow.com/a/30377097/1638254 . You are looking to fill in the OnAuthorization method with suitable code to redirect the user (or let them through
Looking for a way to construct or generate a url for a specific resource in asp.net web api. It can be done in the controller since it inherits from ApiController hence you get the UrlHelper.
I am looking to construct resource url out of the context of the ApiController.
Here is what I did:
Requires HttpContext/Request, so might not work in Application_Start.
Only tested in WebApi 1
Only works for routes registered in GlobalConfiguration (but if you have some other one, just pass it in instead)
// given HttpContext context, e.g. HttpContext.Current
var request = new HttpRequestMessage(HttpMethod.Get, context.Request.Url) {
Properties = {
{ HttpPropertyKeys.HttpConfigurationKey, GlobalConfiguration.Configuration },
{ HttpPropertyKeys.HttpRouteDataKey, new HttpRouteData(new HttpRoute()) },
{ "MS_HttpContext", new HttpContextWrapper(context) }
}
};
var urlHelper = new UrlHelper(request);
What about the UrlHelper classes:
System.Web.Http.Routing.UrlHelper;
System.Web.Mvc.UrlHelper
The MVC one has some useful static methods accepting routing information or it can be used as an instance created by passing in a RequestContext (which is available in most MVC filters and various other places). The instance methods should be exactly what you need to generate urls.
The HTTP one accepts a ControllerContext (which is also available in most HTTP filters and various other places).
I'm not sure about the ApiController, as I haven't used it before. This may then be redundant for you, but then again, it may not be. Check out your Global.asax.cs file, specifically the RegisterRoutes function. Initially, you should see the following mapping:
routes.MapRoute ("Default", "{controller}/{action}/{id}", new { controller = "MyController", action = "Index", id = "" });
So by default your application is set up to handle routes in the following format:
{ControllerName}/{ActionName}/{ResourceId}
A controller class set up like the following should enable you to receive requests in that format.
class {ControllerName}Controller : ApiController
{
public ActionResult {ActionName} (string id)
{
// fetch your resource by its unique identifier
}
}
I am implementing a new ASP.NET MVC 3 application that will use a form of dynamic routing to determine what view to return from a common controller action. I'd like have a default view that is displayed if there is no view at the dynamic location.
Think of it like navigating a tree structure. There is only one TreeController located in the root Controllers folder. It has a Browse action method that accepts the path of the node to browse. Each node can have a custom view so I need to first attempt to locate that view and return it from the action method, like this:
public ViewResult Browse(String path)
{
var model = ...;
return View(path, model);
}
So, if I navigate to "MySite/Tree/A/B/C" then I would expect to find a view at "\Views\Tree\A\B\C.aspx".
However, if there is not a custom view, I need to defer to a standard/default view (such as "\Views\Tree\Browse.aspx").
Since this is only the case for this action method, I don't believe that I should be handling NotFound errors that may result due to other circumstances. And, I'm not looking for dynamic routing as described in other posts because the path to the controller is fixed.
Controllers shouldn't know about physical views.
You do this by writing a custom view engine, e.g.:
public class MyViewEngine: WebFormViewEngine
{
public MyViewEngine()
{
ViewLocationFormats = ViewLocationFormats.Concat(
new [] {"~/Views/{1}/Browse.aspx""}).ToArray();
// similarly for AreaViewLocationFormats, etc., if needed
}
}
See the source code for, e.g., WebFormViewEngine for details.
If you need to do this conditionally (for only a few action) then you can override FindView in that type and look at the route values.
Obviously, if you use Razor, then change that one instead.
Then, in Global.asax.cs, use it:
private void Application_Start(object sender, EventArgs e)
{
// stuff
ViewEngines.Engines.Add(new MyViewEngine());
From within a Controller action this seems to work:
var fullPath = string.Format("~/Views/CustomStuff/{0}.cshtml", viewname);
var mappedPath = Server.MapPath(fullPath);
if( !System.IO.File.Exists(mappedPath) ) return View("Default");
else return View(viewname);
(note: not precompiling views)
I did my research but haven't found any answers.
I'm using Html.RenderAction in a masterpage ( to render page header with links specific to user permissions ). Action is decorated with OutputCache, returns partial control and gets cached as expected.
When the event happens ( let's say permissions are changed ) I want to programmatically invalidate cached partial control.
I'm trying to use RemoveOutputCacheItem method. It takes a path as a parameter. I'm trying to set the path to the action used in Html.RenderAction. That doesn't invalidate the action.
How can I programmatically invalidate the action?
Thanks
The cache for child actions is stored in the OutputCacheAttribute.ChildActionCache property. The problem is that the API generating ids for child actions and storing them in this object is not public (WHY Microsoft??). So if you try to loop through the objects in this collection you will discover that it will also contain the cached value for your child action but you won't be able to identify it unless you reverse engineer the algorithm being used to generate keys which looks something like this (as seen with Reflector):
internal string GetChildActionUniqueId(ActionExecutingContext filterContext)
{
StringBuilder builder = new StringBuilder();
builder.Append("_MvcChildActionCache_");
builder.Append(filterContext.ActionDescriptor.UniqueId);
builder.Append(DescriptorUtil.CreateUniqueId(new object[] { this.VaryByCustom }));
if (!string.IsNullOrEmpty(this.VaryByCustom))
{
string varyByCustomString = filterContext.HttpContext.ApplicationInstance.GetVaryByCustomString(HttpContext.Current, this.VaryByCustom);
builder.Append(varyByCustomString);
}
builder.Append(GetUniqueIdFromActionParameters(filterContext, SplitVaryByParam(this.VaryByParam)));
using (SHA256 sha = SHA256.Create())
{
return Convert.ToBase64String(sha.ComputeHash(Encoding.UTF8.GetBytes(builder.ToString())));
}
}
So you could perform the following madness:
public ActionResult Invalidate()
{
OutputCacheAttribute.ChildActionCache = new MemoryCache("NewDefault");
return View();
}
which obviously will invalidate all cached child actions which might not be what you are looking for but I am afraid is the only way other than of course reverse engineering the key generation :-).
#Microsoft, please, I am begging you for ASP.NET MVC 4.0:
introduce the possibility to do donut caching in addition to donut hole caching
introduce the possibility to easily expire the result of a cached controller action (something more MVCish than Response.RemoveOutputCacheItem)
introduce the possibility to easily expire the result of a cached child action
if you do 1. then obviously introduce the possibility to expire the cached donut portion.
You might want to approach this a different way. You could create a custom AuthorizeAttribute -- it would simply allow everyone -- and add override the OnCacheValidation method to incorporate your logic. If the base OnCacheValidation returns HttpValidationStatus.Valid, then make your check to see if the state has changed and if so, return HttpValidationStatus.Invalid instead.
public class PermissionsChangeValidationAttribute : AuthorizeAttribute
{
public override OnAuthorization( AuthorizationContext filterContext )
{
base.OnAuthorization( filterContext );
}
public override HttpValidationStatus OnCacheAuthorization( HttpContextBase httpContext )
{
var status = base.OnCacheAuthorization( httpContext );
if (status == HttpValidationStatus.Valid)
{
... check if the permissions have changed somehow
if (changed)
{
status = HttpValidationStatus.Invalid;
}
}
return status;
}
}
Note that there are ways to pass additional data in the cache validation process if you need to track the previous state, but you'd have to replicate some code in the base class and add your own custom cache validation handler. You can mine some ideas on how to do this from my blog post on creating a custom authorize attribute: http://farm-fresh-code.blogspot.com/2011/03/revisiting-custom-authorization-in.html