ASP.NET MVC Routing - differences in web site vs virtual directory - asp.net-mvc-3

I have an ASP.NET MVC 3 application (in IIS 7.5) with a portable area. When I host the application consuming the portable area in a web site my routing works perfectly e.g.
http://localhost:9001/Clearance/Home/Search (this works)
However, when hosting in a virtual directory e.g.
http://localhost/Acme.Risks.Clearance.Web.Area.TestUI/Clearance/Home/Search (this does not work)
I get the following error:
Multiple types were found that match the controller named 'Home'. This can happen if the route that services this request ('Clearance/{controller}/{action}') does not specify namespaces to search for a controller that matches the request. If this is the case, register this route by calling an overload of the 'MapRoute' method that takes a 'namespaces' parameter.
I don't understand why, I am specifying namespaces it works fine when running in a web site.
Here is my portable area registration:
public override void RegisterArea(AreaRegistrationContext context, IApplicationBus bus)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
base.RegisterArea(context, bus);
context.MapRoute(
RouteName.ClearanceAreaDefault,
this.AreaName + "/{controller}/{action}/{id}",
new { controller = "Home", action = "Search", id = UrlParameter.Optional },
new[] { typeof(HomeController).Namespace });
}
Here is the Global.asax for the web application in a virtual directory (does not work):
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new[] { typeof(HomeController).Namespace });
}
Here is the Global.asax for the web application in a web site (does work):
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new[] { "Acme.Risks.Web.UI.Controllers" });
}
I have used Phil Haack's "RouteDebugger" (http://nuget.org/packages/routedebugger) and the route debug information on both requests is identical.
Does anyone know why this is happening?
Thanks,
Callum

This was down to the namespace used in the data context for MVC.
My web area assembly is "Acme.Risks.Clearance.Web.Area".
My web site assembly is "Acme.Risks.Web.UI" (working).
My web area test UI assembly is "Acme.Risks.Clearance.Web.Area.TestUI" (not working).
The namespace used in the data context was "Acme.Risks.Clearance.Web.Area*", so in the case of the web area test UI the wildcard caused two matches. So even though the routing information was identical, ASP.NET MVC could not decide which was the right assembly because there were duplicate matches.

Related

Calling Web API from an aspx page is always resulting in Not Found error

I have a web site project ( the one that has an App_Code folder) which I have upgraded to .Net 4.5, and installed NuGet package for Web API 2.2 into the solution in VS 2013.
Under the root folder, there is a folder 'Vendors'. From a page under this folder, I am using jQuery to call a PUT Web API method inside a controller class 'AppsProcureWebApiController' under App_Code folder.
Using the url: 'api/AppsProcureWebApi' in ajax call from jQuery always results in a 'Not Found error'.
But if I hard-code the url as url:'http://localhost/appsprocure/api/AppsProcureWebApi' in same jQuery ajax call then it works and executes the code within the Web API method.
Question: Do I need to use some special routing configuration in global.asax to make it work with orginal url, Or there is something else I need to do? (code being used for configuring routing in global.asax is mentioned below).
jQuery for calling Web API from /Vendors/EditProduct.aspx page
function SaveProdDesc() {
var data = {
productId: $find("<%= radcomBoxProducts.ClientID%>").get_value(),
productDescription: $("#<%= txtProdDesc.ClientID%>").val(),
originalProductDescription: $("#<%= hfOrigProdDesc.ClientID%>").val()
}
$.ajax({
url: 'api/AppsProcureWebApi',
type: 'PUT',
data: JSON.stringify(data),
contentType: "application/json",
dataType:"json",
success: function (data) {
alert(data);
},
error: function (x, y, z) {
alert(x + '\n' + y + '\n' + z);
}
});
}
Routing defined in Global.asax
void Application_Start(object sender, EventArgs e)
{
//below code to make Web API work in Webforms
RouteTable.Routes.MapHttpRoute( name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = System.Web.Http.RouteParameter.Optional });
}
Web API controller class under App_Code is as below.
public class AppsProcureWebApiController : ApiController
{
//OTHER METHODS ARE OMITTED TO SAVE SPACE
[HttpPut]
[Authorize]
public int Put(ProductDesc p)
{
string prodDesc = p.ProductDescription;
return ApplicationDataAccess.UpdateProductDescription(p.ProductId, ref prodDesc, p.OriginalProductDescription);
}
}
public class ProductDesc
{
public long ProductId { get; set; }
public string ProductDescription { get; set; }
public string OriginalProductDescription { get; set; }
}
I found the answer after a lot of trying. This will be very useful in cases when using jQuery from an aspx page to call Web API in webforms asp.net projects, since in such projects the pages will exist under different folders.
Only a simple change is needed so the Web API can be called seamlessly from an aspx page under folder 'Vendors' using the url: 'api/AppsProcureWebApi'.
This simple change is of adding vendors to the routing configuration. If you let the original rule be there then make sure you name this new routing rule differently i.e. something other than DefaultApi. I have named this new rule as Rule1Api in code below.
So api/{controller}/{id} becomes vendors/api/{controller}/{id} in routing configuration as in code below. But do not change the url mentioned in jQuery call, which means let it be url: api/AppsProcureWebApi since vendors will be automatically prepended to the url mentioned in jQuery call.
void Application_Start(object sender, EventArgs e)
{
//below code to make Web API work in Webforms
RouteTable.Routes.MapHttpRoute( name: "Rule1Api",
routeTemplate: "vendors/api/{controller}/{id}",
defaults: new { id = System.Web.Http.RouteParameter.Optional });
}

SelfHosted AspNet WebAPI With Controller Classes In Different Project

I have created a SelfHosted AspNet WebAPI with Visual Studio 2012 (.NET Framework 4.5). I enabled SSL for the WebAPI. It works fine when the controller is defined in the same project.
But when I add a reference of another project, containing controllers, it gives me the following error:
No HTTP resource was found that matches the request URI 'https://xxx.xxx.xxx.xxx:xxxx/hellowebapi/tests/'.
I have created custom classes for HttpSelfHostConfiguration and MessageHandler.
Any help to resolve this problem would be a great time-savor for me.
Thanking in advance.
You can write a simple custom assemblies resolver which makes sure that your referenced assembly is loaded for the controller probing to work.
Following is a nice post from Filip regarding this:
http://www.strathweb.com/2012/06/using-controllers-from-an-external-assembly-in-asp-net-web-api/
Sample:
class Program
{
static HttpSelfHostServer CreateHost(string address)
{
// Create normal config
HttpSelfHostConfiguration config = new HttpSelfHostConfiguration(address);
// Set our own assembly resolver where we add the assemblies we need
CustomAssembliesResolver assemblyResolver = new CustomAssembliesResolver();
config.Services.Replace(typeof(IAssembliesResolver), assemblyResolver);
// Add a route
config.Routes.MapHttpRoute(
name: "default",
routeTemplate: "api/{controller}/{id}",
defaults: new { controller = "Home", id = RouteParameter.Optional });
HttpSelfHostServer server = new HttpSelfHostServer(config);
server.OpenAsync().Wait();
Console.WriteLine("Listening on " + address);
return server;
}
static void Main(string[] args)
{
// Create and open our host
HttpSelfHostServer server = CreateHost("http://localhost:8080");
Console.WriteLine("Hit ENTER to exit...");
Console.ReadLine();
}
}
public class CustomAssembliesResolver : DefaultAssembliesResolver
{
public override ICollection<Assembly> GetAssemblies()
{
ICollection<Assembly> baseAssemblies = base.GetAssemblies();
List<Assembly> assemblies = new List<Assembly>(baseAssemblies);
var controllersAssembly = Assembly.LoadFrom(#"C:\libs\controllers\ControllersLibrary.dll");
baseAssemblies.Add(controllersAssembly);
return assemblies;
}
}

Configuring IIS 6.0 to run an MVC3 application

Configuring IIS 6.0 to run an MVC3 application
I think I have a configuration issue on my IIS 6 server and I'd like to see if there's anything I've missed.
The problem that I'm having is that anytime when a RedirectToAction("Index", "Home") is executed (e.g. in a method that returns an ActionResult) I would expect that I would be returned to:
http://servername.domain.com/virtualdirectoryname
However, instead I get redirected to:
http://servername.domain.com/virtualdirectoryname/virtualdirectoryname
That is a second instance of the virtualdirectoryname appended to the end of the URL and can't figure out why - this URL will of course yield a 404 resource not found error. I written and deployed several MVC3 applications both in corporate intranet and public internet environments and can't figure out what I've done wrong. My global.asax.cs seems ok -
public class MvcApplication : System.Web.HttpApplication
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
}
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
);
}
protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
if (HttpContext.Current.User != null)
{
if (HttpContext.Current.User.Identity.IsAuthenticated)
{
if (HttpContext.Current.User.Identity is FormsIdentity)
{
FormsIdentity id = (FormsIdentity)HttpContext.Current.User.Identity;
FormsAuthenticationTicket ticket = id.Ticket;
// Get the stored user-data, in this case, our roles
string userData = ticket.UserData;
string[] roles = userData.Split(',');
HttpContext.Current.User = new System.Security.Principal.GenericPrincipal(id, roles);
}
}
}
}
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
}
The Application_AuthenticateRequest handles the storing of the roles for logged on users, but other than that, it seems pretty vanilla. The only think I can think of is that I've somehow messed up the virtual directory.
Prior to performing any of these steps, I had verified that MVC3 and v4.0 of the .NET framework were installed on the server. There are also other ASP.NET 4.0 applications on this server that have been running without incident. There is also an MVC2 application (MVC2 is also installed) running on this server and has been running without incident.
I created a virtual directory off of the main "default site" using the IIS manager.
Setup appropriate permissions on the folder that this virtual directory points to. Tested with a quick "Hello, World" index.html file.
Copied the application from my development PC where the application works as developed to the folder described in #2.
Updated the Web.Config file, editing the connection strings to point to the test database server; I had also verified these connection strings on my development PC.
Open the web browser and hope for the best.
Any assistance is greatly appreciated.
Thanks!
I think what you may be seeing is:
http://servername.domain.com/virtualdirectoryname/applicationname
If you have named your virtual directory the same name as your application then I could see how that could confuse you. If you had no virtual directory and just your application at the root of the Default Web Site you'd be seeing:
http://servername.domain.com/applicationname
Is your virtual directory the same name as your application name? If so, that is why you see this.

Handling URL's with AppHarbor without Modifying All My Controllers

I'm trying to host an MVC 3 application (FunnelWeb) on AppHarbor. For a reason that's still not clear to me, when my route is only a Controller+Action (e.g. mysite/admin is Admin+Index and mysite/login is Admin+login) everything works fine, but if I have anything else in the route (e.g. a variable like {*page}) my URL will be mysite:12345/mypage (where 12345 is a port number assigned by AppHarbor and mypage is the name of the page I'm requesting). This makes the request fail as the port 12345 is not publicly exposed.
AppHarbor uses load balancing to distribute the request between multiple IIS's. This is their way of doing stuff and this is why internally the requests are routed to some non-standard ports. I don't have a problem with that, but I have problem with MVC that tries to route me to that internal URL.
I'm not pointing fingers here; it's nobody's fault :) so let's move to the question:
Why there is a difference between requesting a route with Controller+Action only and requesting a route with a variable like {*page}? Be technical please :)
Here is an example of how to handle requests in AppHarbor, however, it seems that it requires me to modify all my controllers (OMG). Is there any way to implement this without modifying my controllers?
Any other suggestions are welcomed :)
Thanks in advance.
UPDATE: Coincidentally, the behaviour that I observed matches the conclusion that I reached. However, the issue has nothing to do with ASP.Net MVC routing. The short story is, FunnelWeb forces lowercase URL's, so, whenever it receives a request to a resource it convert it to lowercase, if needed, and issue a 301 response. The problem is, when creating the URL for the 301 response, the request URL (absolute URL) is now the URL used when the request made from the load balancer to IIS and not the one made from the client; hence, the request fails.
This is known issue with FunnelWeb url generation on AppHarbor. When using standard MVC methods to generate relative URLs, this is not a problem. AppHarbor has a short guide and sample on how the generate public URLs in the knowledge base.
It's possible that the following is now all you need:
<appSettings>
<!-- AppHarbor Setting to stop AppHb load balancer internal port numbers from showing up in URLs-->
<add key="aspnet:UseHostHeaderForRequestUrl" value="true" />
</appSettings>
This is noted as an update on AppHarbor's support page at http://support.appharbor.com/kb/getting-started/workaround-for-generating-absolute-urls-without-port-number
MSDN says the following about UseHostHeaderForRequestUrl:
aspnet:UseHostHeaderForRequestUrl - If this value attribute is false [default], the Url property is dynamically built from the host, port, and path provided by the web server. If this value attribute is true, the Url property is dynamically built by using the host and port provided by the incoming "Host" header and the path provided by the web server.
There is a way, but it requires a couple of classes.
When ASP.NET MVC registers a route, it defines a route handler. This route handler returns a HTTP handler that handles the request. If you use a custom route handler that returns a custom HTTP handler, you can rewrite the HTTP context by using a couple decorator classes.
Start by creating a HttpContextProxy and HttpRequestProxy that derives from the base classes and wraps all methods and properties to an inner instance. I've made the hard work available.
Next create the decorators, first the HTTP context decorator:
using System.Web;
public class HttpContextDecorator : HttpContextProxy
{
public HttpContextDecorator(HttpContextBase innerHttpContext)
: base(innerHttpContext)
{
}
public override HttpRequestBase Request
{
get
{
return new HttpRequestDecorator(base.Request);
}
}
}
The HTTP request decorator:
using System;
using System.Web;
public class HttpRequestDecorator : HttpRequestProxy
{
public HttpRequestDecorator(HttpRequestBase innerHttpRequest)
: base(innerHttpRequest)
{
}
public override bool IsSecureConnection
{
get
{
return string.Equals(Headers["X-Forwarded-Proto"], "https", StringComparison.OrdinalIgnoreCase);
}
}
public override Uri Url
{
get
{
var url = base.Url;
var urlBuilder = new UriBuilder(url);
if (IsSecureConnection)
{
urlBuilder.Port = 443;
urlBuilder.Scheme = "https";
}
else
{
urlBuilder.Port = 80;
}
return urlBuilder.Uri;
}
}
public override string UserHostAddress
{
get
{
const string forwardedForHeader = "HTTP_X_FORWARDED_FOR";
var forwardedFor = ServerVariables[forwardedForHeader];
if (forwardedFor != null)
{
return forwardedFor;
}
return base.UserHostAddress;
}
}
}
As mentioned, you also need to override the MVC classes - here the HTTP handler:
using System;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
public class CustomMvcHandler : MvcHandler
{
public CustomMvcHandler(RequestContext requestContext)
: base(requestContext)
{
requestContext.HttpContext = new HttpContextDecorator(requestContext.HttpContext);
}
protected override IAsyncResult BeginProcessRequest(HttpContextBase httpContext, AsyncCallback callback, object state)
{
httpContext = new HttpContextDecorator(httpContext);
return base.BeginProcessRequest(httpContext, callback, state);
}
protected override void ProcessRequest(HttpContextBase httpContext)
{
httpContext = new HttpContextDecorator(httpContext);
base.ProcessRequest(httpContext);
}
}
Then the route handler:
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
public class CustomMvcRouteHandler : MvcRouteHandler
{
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new CustomMvcHandler(requestContext);
}
}
Finally, you'll need to replace the associated handler for all registered routes (or map them properly from the beginning):
var routes = RouteTable.Routes.OfType<Route>().Where(x => x.RouteHandler is MvcRouteHandler);
foreach (var route in routes)
{
route.RouteHandler = new CustomMvcRouteHandler();
}

asp.net mvc 3 and static resources 404'ing when using "Local IIS Web server"

Here's the situation:
ASP.NET MVC 3 application using Razor as the view engine.
Works fine under Visual Studio Development Server (Cassini)
However, when I switch over to "Use Local IIS Web server", the site functions, but every static resource 404s (again, there were no issues under Cassini).
ASP.NET 4.0, Windows 7 Ultimate x64, IIS 7.5, Integrated Pipeline, Network Service as app pool identity.
Specifically, an exception for trying to access a static file that is known to exist (i.e. remove application files, specifically DLLs with route information, etc. and it is served up without issue). Again, this occurs for all static files including i.e. /public/scripts/jquery.js:
Error in Path :/favicon.ico
Raw Url :/favicon.ico
Message :Path '/favicon.ico' was not found.
Source :System.Web
Stack Trace : at System.Web.HttpNotFoundHandler.ProcessRequest(HttpContext context)
at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)
TargetSite :Void ProcessRequest(System.Web.HttpContext) NLogLogger.Fatal => NLogLogger.Fatal => LoggerImpl.Write
I'm baffled. I've verified that a test default ASP.NET MVC 3 app runs fine under both VS Development Server and Local IIS Web server on this machine.
My hope is that someone else has run into a similar issue. In case it helps, here are my routes:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("favicon.ico");
routes.MapRoute(
"Login", // Route name
"login", // URL with parameters
new { controller = "Session", action = "Create" } // Parameter defaults
);
routes.MapRoute(
"Logout", // Route name
"logout", // URL with parameters
new { controller = "Session", action = "Delete" } // Parameter defaults
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}
Of course, it turns out to be something extremely simple: there was an HttpHandler defined in the root Web.config instead of just the View directory's Web.config
<handlers>
<remove name="BlockViewHandler" />
<add name="BlockViewHandler" path="*" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler" />
</handlers>
Obvious now, but hopefully by posting here it might save somebody else wasting time on something so obvious. See http://haacked.com/archive/2008/06/25/aspnetmvc-block-view-access.aspx for more info.

Resources