I have an ASP.NET / C# website that's hosted on Windows Azure. The site is a predictions-based social site with a feed of prediction summaries on the main page. If you click on a summary, you're redirected to the details page for that prediction using a simple QueryString.
For example:
http://www.ipredikt.com/details.aspx?id=14
This particular prediction is entitled "Paris Hilton will win the Nobel Peace Prize" so what I'd like to do is implement URL rewriting for my site on Azure as follows:
http://www.ipredikt.com/predictions/14/paris-hilton-will-win-the-nobel-peace-prize
What are some strategies and best practices for doing this? And can someone point me to a good Azure-specific article or two.
The hyphenated title ("paris-hilton-bla-bla") is really just to make the URL more human readable; I don't envision relying on it at all in terms of loading pages. In fact, I'd probably allow duplicate titles since I'll be relying on the prediction ID in the URL.
EDIT:
Forgot to mention that we are NOT based on MVC. We came up w/ our own architecture that uses PageMethods and WebMethods to return JSON to the client. We rely on ASP.NET AJAX to do all of the JSON serialization, and almost all of our UI is built dynamically on the client using jQuery.
EDIT: SOLUTION
Thought I'd share my solution now that I have things up and running.
I made a new class as follows (copied verbatim from somewhere):
public class WebFormRouteHandler<T> : IRouteHandler where T : IHttpHandler, new()
{
public string VirtualPath { get; set; }
public WebFormRouteHandler(string virtualPath)
{
this.VirtualPath = virtualPath;
}
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return (VirtualPath != null)
? (IHttpHandler)BuildManager.CreateInstanceFromVirtualPath(VirtualPath, typeof(T))
: new T();
}
}
I added the following method to Global.asax. The actual method is MUCH, much longer (it covers every page in the site). You'll see that I support calling the predictions page in lots of different ways: with an id, with an id + title, etc. (The "...fb" versions of pages are for the Facebook app version of my site which use a different MasterPage.)
public static void RegisterRoutes(RouteCollection routes)
{
// Details : 'predictions' Page
var routeHandlerDetails = new WebFormRouteHandler<Page>("~/details.aspx");
var routeHandlerDetailsFb = new WebFormRouteHandler<Page>("~/detailsfb.aspx");
routes.Add(new Route("predictions/{id}", routeHandlerDetails));
routes.Add(new Route("predictions/{id}/{title}", routeHandlerDetails));
routes.Add(new Route("fb/predictions/{id}", routeHandlerDetailsFb));
routes.Add(new Route("fb/predictions/{id}/{title}", routeHandlerDetailsFb));
}
...and this method is called from Application_Start()
void Application_Start(object sender, EventArgs e)
{
RegisterRoutes(RouteTable.Routes);
}
Then I added the following to web.config in the system.webServer block:
<!-- Added for URL Routing -->
<modules runAllManagedModulesForAllRequests="true">
<add name="UrlRoutingModule"
type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
</modules>
<!-- Added for URL Routing -->
<handlers>
<add name="UrlRoutingHandler"
preCondition="integratedMode"
verb="*"
path="UrlRouting.axd"
type="System.Web.HttpForbiddenHandler, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
</handlers>
I also had to exclude the virtual "predictions" directory from authentication (because almost all parts of our site are accessible my non-auth users):
<!-- Url routing -->
<location path="predictions">
<system.web>
<authorization>
<allow users="*" />
</authorization>
</system.web>
</location>
Finally, I no longer rely on QueryString string parameters when loading pages, so I had to write some new helper methods. Here's one that extracts a numerical value from the new routing URL (I'll be cleaning this up to only have a single 'return'.):
public static int GetRouteDataValueAsNumber(HttpRequest request, string propertyName)
{
if ((request == null) ||
(request.RequestContext == null) ||
(request.RequestContext.RouteData == null) ||
(request.RequestContext.RouteData.Values[propertyName] == null))
{
return -1;
}
try
{
return System.Convert.ToInt32(request.RequestContext.RouteData.Values[propertyName]);
}
catch
{
}
return -1;
}
Now when I need to read a routing value (like a prediction ID), I do the following:
long _predictionId = System.Convert.ToInt64(WebAppUtils.GetRouteDataValueAsNumber(Request, "id"));
Works great! Now my site feels like an MVC app with friendly and self-documenting URLs.
Oh, last thing, you also need to enable HTTP Redirection as follows:
Start => Control Panel => Program => Turns Windows Features On => Internet Information Services => World Wide Web Services => Common HTTP Features => (select checkbox for) HTTP Redirection.
The easiest way to implement this would be a programmatic approach using the System.Web.Routing assembly.
This basically works by including the UrlRoutingModule in your web.config, and defining patterns that resolve the target page based on matching routes. If you are familiar with ASP.NET MVC, then you have used this routing strategy before, but MVC is not necessary to use Routing.
Here are some resources to help you get started:
MSDN Documentation for the System.Web.Routing namespace - official documentation
Scott Gu on URL Routing for MVC - * Note that this article explains routing in the context of an ASP.NET MVC application, however, the same methodology will work regardless of whether or not you are using MVC
ASP.NET Routing... Goodbye URL rewriting, by Chris Cavanagh - An explanatory article
Exploring System.Web.Routing, by Justin Etheredge - A case study explaining how to use routing independently of the MVC architecture
About Windows Azure ...
If you take this approach, it doesn't really matter that you are using Windows Azure. However, I found an article by Michael Kennedy called ASP.NET Routing in Windows Azure Using WebForms, explaining how to easily deploy such a solution on Windows Azure. The article even has a sample project for download.
Azure web roles have the IIS7 Url Rewriting module installed - http://msdn.microsoft.com/en-us/library/dd573358.aspx
The "how to" for this module is at http://learn.iis.net/page.aspx/460/using-the-url-rewrite-module/
For your Paris example, you basically need to setup a rule that maps the url
http://www.ipredikt.com/predictions/14/paris-hilton-will-win-the-nobel-peace-prize
to
http://www.ipredikt.com/details.aspx?id=14
This is something like:
Pattern -
^predictions/([0-9]+)/([_0-9a-z-]+)
Action -
details.aspx?id={R:1}
For more on defining these rules see http://learn.iis.net/page.aspx/461/creating-rewrite-rules-for-the-url-rewrite-module/
Related
I'm trying to implement a custom http security module that uses the roles in the sitemap to control access to pages (instead of having to store it all in the web.config as well). Following article here: http://www.codeproject.com/Articles/8728/Extending-ASP-NET-security
I've updated it for the newer versions of IIS, adding the module in system.webServer instead
<system.webServer>
<modules>
<add name="SecurityHttpModule" type="DINO.SecurityHttpModule"/>
</modules>
</system.webServer>
Everything seems to be working alright in respect to that, but pages are no longer rendering correctly. If I look at the console in Chrome I am seeing errors like
Resource interpreted as Stylesheet (or Script) but transferred with MIME type test/html: "http://localhost:57855/login"
and
Uncaught SyntaxError: Unexpected token < (about the <!DOCTYPE html> at the top of the page)
I assume I'm just missing something else I need to do when I'm adding a custom module, but I have not yet been able to find any reference to this issue.
Oguz Ozgul was correct in his comment. To fix it, I added a list of extensions I want to validate permissions for and then I check that as the first part of my authenticate request method.
private static readonly List<string> extensionsToValidate = new List<string>(new string[] { ".aspx", "" });
private void AuthenticateRequest(Object sender, EventArgs e)
{
//Ignore specified extensions from redirection
string CurrentExt = Path.GetExtension(HttpContext.Current.Request.Url.LocalPath);
if (extensionsToValidate.Contains(CurrentExt))
{
//do all security check work here
}
else return;
}
I am trying to serve some JS and CSS files that are embedded into a DLL, with a solution based on this approach here: http://weblogs.asp.net/imranbaloch/asp-net-bundling-and-minification-and-embedded-resources
so, javascript and css files are embedded and I create bundles for them.
My problems start because, having quite a few of them, I need some folder structure to keep order. So the original route
RouteTable.Routes.Insert(0,
new Route("Embedded/{file}.{extension}",
new RouteValueDictionary(new { }),
new RouteValueDictionary(new { extension = "css|js" }),
new EmbeddedResourceRouteHandler()
));
is not enough anymore, so I have changed it to this:
RouteTable.Routes.Insert(0,
new Route("Embedded/{*url}",
new RouteValueDictionary(new { }),
new EmbeddedResourceRouteHandler()
));
I also cannot use the extension part because the catch-all part has to be the last one
So now if I try to access anything that looks like a file, my route will never be used so I will just get a 404
I have tried replacing the dot with a slash or adding a slash at the end but what I'm after here is a simple solution that will allow me to map urls that look like files to actual files.
I've also searched the web and there seem to be solutions based on UrlRewrite or altering the web.config but:
- I would like not to modify the IIS settings for every application to accomodate the library
- since it's a library, I would like it to be self contained and developers that use it shouldn't care about these sort of internal issues
So, is there a solution that I can implement in my library for this?
Also worth mentioning is that the original routing had the same issue, it only worked because of
<modules runAllManagedModulesForAllRequests="true" />
in the web.config, which I don't think is a good idea for performance
When you set
<modules runAllManagedModulesForAllRequests="true" />
this enables all available modules to run against the request. Which, as you mentioned, isn't the best for performance. However, you could add only the module you actually need- in this case the UrlRoutingModule.
You could add this module like this:
<system.webServer>
<modules>
<remove name="UrlRoutingModule-4.0" />
<add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule" preCondition="" />
</modules>
</system.webServer>
If you want an even better way (IMO) to do this, disregard the WebConfig and add it in a AppStart.cs file in your class library.
using Microsoft.Web.Infrastructure.DynamicModuleHelper;
[assembly: WebActivatorEx.PreApplicationStartMethod(typeof(AppStart), "PreStart")]
[assembly: WebActivatorEx.PostApplicationStartMethod(typeof(AppStart), "Start")]
namespace EmbeddedPages
{
public static class AppStart
{
private static bool PreStartFired = false;
public static void PreStart()
{
if (!PreStartFired)
{
PreStartFired = true;
DynamicModuleUtility.RegisterModule(typeof(UrlRoutingModule));
}
}
}
}
This adds the UrlRoutingModule into the module stack, and your URL's should now properly resolve. Note: you will need to add WebActivator to your project through nuget.
My Question Is This
What configuration step have I missed to make Mvc Surface Controllers work in Umbraco?
My theory is that since there is a folder in the default Umbraco install called /umbraco/ which is used to connect to the CMS that the physical path is interfiering with the route /umbraco/surface/{Controller}/{Action} thus resulting in the ASP.NET YSOD (and an IIS 404
when I try to access a controller on that route that isn't defined.)
Background Information
I have added this class to my App_Code folder in a freshly downloaded copy of Umbraco 6.1.6:
public class MembersController : SurfaceController
{
public ActionResult Index()
{
return Content("Hello, Member!");
}
}
When I navigate to what I think should be the route for my Index() method, I get a YSOD that says the resource could not be found:
the code is not executed and the above error is displayed; however, if I change the Uri to garbage I get an IIS 404 error:
I started getting this in an existing site, thinking my site was screwed up I tried it in a new copy of Umbraco 6.1.6 and got the exact same results.
For the record, I have also tried MembersSurfaceController and its associated Uri, which has the exact same result as above. YSOD when I hit the valid route, and IIS 404 when I don't.
I have changed my umbracoSettings.config to MVC in the /config/ directory as well.
update
I'm using the out-of-the-box web.config file, which has this:
<system.webServer>
<validation validateIntegratedModeConfiguration="false" />
<modules runAllManagedModulesForAllRequests="true">
<remove name="UrlRewriteModule" />
<add name="UrlRewriteModule" type="UrlRewritingNet.Web.UrlRewriteModule, UrlRewritingNet.UrlRewriter" />
.
..
...
On my default Umbraco site I don't have any rewrite rules defined; but on my actual site I have several rewrite rules in place. I'm thinking that's not causing it since I'm seeing the same behavior on both sites though...
I have tried removing UrlRewrite completely I get the same results.
The following approach works for me in Umbraco 7.1, and I expect it to work in 6.1 as well:
Create folder called 'Controllers' within your App_Code folder, and put your surface controllers in there (so that they will be within the 'Controllers' namespace).
E.g. I have the following controller in the App_Code\Controllers folder (and hence, within the 'Controllers' namespace):
namespace Controllers
{
public class ServiceCentersController : SurfaceController
{
public ActionResult GetServiceCenters(string country = "", string region = "", string city = "")
{
...
}
}
}
My site runs on localhost, so I can invoke the GetServiceCenters action by navigating to:
http://localhost/umbraco/Surface/ServiceCenters/GetServiceCenters?country=aa®ion=bb&city=cc
You need a namespace for your controller - the code posted above doesn't have any namespace:
public class MembersController : SurfaceController
{
public ActionResult Index()
{
return Content("Hello, Member!");
}
}
That is why making a namespace of Controllers works ... but you could make this any logically named namespace you want.
I am trying to get this to work. I have a DNN module in which I read from a querystring and perform a few steps. All of that is working fine. Now I am trying to clean up the URL while reading the querystring
Right now, the URL looks something like this:
http://mysite.website.com/?pid=1234
I would like it to look like:
http://mysite.website.com/1234
Is something like this even possible?
You are much better to use a proper rewriting solution for DotNetNuke (e.g. iFinity UrlMaster and there are others...).
You can then write a custom url provider for your module.
That's what I've done on my site to rewrite parts of my articles module (e.g. www.ventrian.com/blog/
You can find more information about urlmaster here:
http://www.ifinity.com.au/Products/Url_Master_DNN_SEO_Urls
look at using a URL Rewriter module. There are several third party ones for IIS6, but Microsoft provides one for IIS7 and IIS7.5. You basically configure it with a regular expression and change the output.
Microsoft's rewrite module for IIS7 is available at: http://www.iis.net/downloads/microsoft/url-rewrite
You've got a couple of choices:
Explore the rewrite capabilities available in DNN and how to use them. They can be found in Host Settings > Advanced Settings > Friendly URL Settings. Or use the 2nd option based on which version of IIS you're working on.
2a. URL Rewrite Module for IIS 7 & above
2b. "ISAPI_Rewrite 3" by HeliconTech (has free version too, that does the job pretty well)
You can accomplish what you are looking for without interacting with DNN at all by using an HttpModule. Kind of like this:
public class PidRewriteModule : System.Web.IHttpModule
{
public void Dispose()
{
}
public void Init(System.Web.HttpApplication context)
{
context.BeginRequest += new EventHandler(context_BeginRequest);
}
void context_BeginRequest(object sender, EventArgs e)
{
HttpApplication app = sender as HttpApplication;
if (app != null)
{
Match mPidCheck = new Regex(#"^/(?<pid>[0-9]+)/?$").Match(app.Context.Request.Url.AbsolutePath);
if (mPidCheck.Success)
{
app.Context.RewritePath("~/default.aspx", String.Empty, String.Concat("pid=", mPidCheck.Groups["pid"].Value));
}
}
else
return;
}
}
Then you can add this to your Web.config:
<modules runAllManagedModulesForAllRequests="true">
<add name="PidRewriteModule" type="Assembly.Namespace.PidRewriteModule, Assembly"/>
</modules>
Put that in the system.webServer node. Substitute Assembly and Namespace respectively.
All of this info is for IIS7. It's not entirely different for IIS 6, but previous implementations you have to go the route of ISAPI filters.
I am just learning ASP.NET MVC, and I have recently found the [RequireHttps] attribute to automatically redirect a GET request to use SSL, like so...
[RequireHttps] //apply to all actions in controller
public class SomeController
{
[RequireHttps] //apply to this action only
public ActionResult SomeAction()
{
...
}
}
When using IIS Express as the development server, this successfully redirects the request from http://localhost:55945/... to https://localhost/....
However on my development system, my project is using HTTPS on port 44300 (this was configured automatically by Visual Studio 2010), and I have not yet found any way to tell MVC to use that port number in the redirection, so that it goes to the required https://localhost:43300/... instead.
I really expected this to be automatic, given the SSL port number was set automatically by Visual Studio, and as far as I can tell, this must be effecting all developers using the [RequireHttps] attribute in MVC3. In my searching for a solution, I have seen a few patchy "work around" solutions, but nothing that appears to be definitively "the right way" to fix it.
So doing things "the right way", what do I change (either in the source code, or in my project configuration) to tell the [RequireHttps] attribute in MVC3 to use the HTTPS port that my project is configured to be using?
Or alternatively, is there some other completely different and better "right way" to set up SSL support in an MVC3 project, that does not have this problem?
The RequireHttpsAttribute is quite simple and it cannot be parametrized to redirect to a specific port. If you really need this you could create a subclass and override the HandleNonHttpsRequest method to compose the redirect url differently.
protected override void HandleNonHttpsRequest(AuthorizationContext filterContext) {
base.HandleNonHttpsRequest(filterContext);
// redirect to HTTPS version of page
string url = "https://" + filterContext.HttpContext.Request.Url.Host + ":" + MyConfig.SslPort + filterContext.HttpContext.Request.RawUrl;
filterContext.Result = new RedirectResult(url);
}
However, if your entire site runs with HTTPS you could just configure in VS in the web project properties (Web -> Start Action -> Start URL) to open the correct HTTPS url with your port and not use the redirect feature for local testing.
I have this same issue, and I'm solving it by using both the RequireHttpsAttribute and a URL rewrite rule in Web.config. The rewrite rule matches the nonstandard port numbers, and executes ahead of the attribute. You can use a Web.config transform to remove the rewrite rule on deployment, but if you leave it in it shouldn't have any effect. In production you'll use standard port number that the rule won't match. Then the attribute will catch it.
Here's the rule:
<system.webServer>
<rewrite>
<rules>
<!-- Redirect HTTP requests to HTTPS, using the non-standard development ports.
On deployment, this rule is removed, and the RequireHttpAttribute filter globally applied
in SlicerWeb.FilterConfig takes over. This rewrite rule executes before the attribute
would be applied, and so can apply the port numbers -->
<rule name="HTTPS redirect" stopProcessing="true">
<match url="(.*)" />
<conditions>
<add input="{HTTPS}" pattern="off" ignoreCase="true" />
<add input="{SERVER_PORT}" pattern="60470" />
</conditions>
<action type="Redirect" url="https://{SERVER_NAME}:44300/{R:1}" redirectType="Found" />
</rule>
</rules>
</rewrite>
</system.webServer>
The downside to this approach is that you're not relying on the attribute when running locally. But if you've applied it once globally, instead of adding it to every controller, that's OK in my opinion.
A few things that might be helpful.
There is a version of the source code of RequireHttpsAttribute in this thread: Where is the source for RequireHttpsAttribute?
There is also a nearly identical class called RequireSslAttribute on codeplex mentioned on the same thread. http://aspnet.codeplex.com/SourceControl/changeset/view/63930#391756
Here is a sample of an attribute that can be used to toggle either from http to https or the reverse based on the TargetUriScheme property. It also includes properties for specifying port numbers.
I have chosen to use an #if DEBUG block in my constructor to set my local development ports when I am building under the Debug configuration. This works for me since I always build under release when deploying, in which case the port numbers will default to null and will be left out of the url's.
The port numbers can also be set when applying the attribute to an action method. I could also see hooking these to a config file or some other configuration source to determine the port numbers at runtime.
public class ToggleHttpHttpsAttribute : FilterAttribute, IAuthorizationFilter
{
//supported uri scheme values
public enum UriScheme
{
Http,
Https
}
public ToggleHttpHttpsAttribute(
UriScheme uriScheme = UriScheme.Http)
{
TargetUriScheme = uriScheme;
#if DEBUG
//set DEBUG ports
HttpPort = 55892;
HttpsPort = 44301;
#endif
}
private UriScheme TargetUriScheme { get; set; }
public int? HttpPort { get; set; }
public int? HttpsPort { get; set; }
public void OnAuthorization(AuthorizationContext filterContext)
{
if(filterContext == null)
{
throw new ArgumentNullException("filterContext");
}
bool isHttps = filterContext.HttpContext.Request.IsSecureConnection;
if ((isHttps && TargetUriScheme == UriScheme.Http) || (!isHttps && TargetUriScheme == UriScheme.Https))
{
ToggleUriScheme(filterContext);
}
}
private void ToggleUriScheme(AuthorizationContext filterContext)
{
//only allow toggle if GET request
if (!string.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("ToggleHttpHttpsAttribute can only be used on GET requests.");
}
filterContext.Result = GetRedirectResult(filterContext);
}
private RedirectResult GetRedirectResult(AuthorizationContext filterContext)
{
string prefix = TargetUriScheme == UriScheme.Http
? "http://"
: "https://";
int? port = TargetUriScheme == UriScheme.Http
? HttpPort
: HttpsPort;
string url = string.Format(
"{0}{1}{2}{3}",
prefix,
filterContext.HttpContext.Request.Url.Host,
port == null ? "" : string.Format(":{0}", port),
filterContext.HttpContext.Request.RawUrl);
return new RedirectResult(url);
}
}
I'm quite puzzled as to why I have had to spend hours trying to get this right. I did a URL rewrite similar to the way Scott Hanselman explains it here:
http://www.hanselman.com/blog/WorkingWithSSLAtDevelopmentTimeIsEasierWithIISExpress.aspx
This works perfectly and I just take out the rewrite rule on my production web server.