Can't handle routing correctly in mixed VS environment - asp.net

I am creating a new project in Visual Studio using the SPA template. My goal is to have an Angular/SPA application that will "host"/contain some legacy applications that will eventually be modernized/migrated. So, I have an iframe on a page in my SPA app, and when a menu item is clicked, I want to load one of the legacy ASP.NET apps in that iframe (it has to be in an iframe, as the legacy site used them, and its architecture relies on them).
I am having trouble getting the routing right. The SPA template defines a DefaultRoute class like this (I changed RouteExistingFiles to true):
public class DefaultRoute : Route
{
public DefaultRoute()
: base("{*path}", new DefaultRouteHandler()) {
this.RouteExistingFiles = true;
}
}
and I have edited the RouteConfig.cs file to ignore "aspx" page requests, like this:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes) {
routes.Ignore("*.aspx");
routes.Add("Default", new DefaultRoute());
}
}
The default route handler that is defined, looks like this:
public class DefaultRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext) {
// Use cases:
// ~/ -> ~/views/index.cshtml
// ~/about -> ~/views/about.cshtml or ~/views/about/index.cshtml
// ~/views/about -> ~/views/about.cshtml
// ~/xxx -> ~/views/404.cshtml
var filePath = requestContext.HttpContext.Request.AppRelativeCurrentExecutionFilePath;
if (filePath == "~/") {
filePath = "~/views/index.cshtml";
}
else {
if (!filePath.StartsWith("~/views/", StringComparison.OrdinalIgnoreCase)) {
filePath = filePath.Insert(2, "views/");
}
if (!filePath.EndsWith(".cshtml", StringComparison.OrdinalIgnoreCase)) {
filePath = filePath += ".cshtml";
}
}
var handler = WebPageHttpHandler.CreateFromVirtualPath(filePath); // returns NULL if .cshtml file wasn't found
if (handler == null) {
requestContext.RouteData.DataTokens.Add("templateUrl", "/views/404");
handler = WebPageHttpHandler.CreateFromVirtualPath("~/views/404.cshtml");
}
else {
requestContext.RouteData.DataTokens.Add("templateUrl", filePath.Substring(1, filePath.Length - 8));
}
return handler;
}
}
The directory structure is like this:
MySPA
SPA <- contains the SPA application (.net 4.5)
Legacy <- contains the legacy applications (.net 3.0)
In IIS, I have the legacy folder set as a virtual directory (subdirectory) within the SPA application.
How do I set up routing so that, when a menu item is clicked, and the request is sent (containing a url that has query string information) for an .aspx page, the request can be routed to the legacy application?

I have solved this issue. The issue was caused by several problems. But, the problem that most pertains to my information above was this...I needed to find the correct way to ignore the requests for the legacy aspx pages from within the routing code of the new site. So, in the RouteConfig.cs file, I placed this ignore statement as the first line of the RegisterRoute function:
routes.Ignore("{*allaspx}", new { allaspx = #".*\.aspx(/.*)?"});
That corrected the main issue, there were other minor issues that confused the situation but, I have identified what they are and I am working on those. So, ignoring the legacy urls in the routing functionality was the correct solution.

Related

Copying raw html from a file into a razor page, in aspnet core?

I'm moving an existing ASP.NET/MVC app to aspnet core, and there's a bit where I'm not sure of the cleanest solution.
The issue is that we have a shared view that is called from a number of controllers. There's a chunk of html that is loaded from a file by the controller, and then is inserted into the page by the view using #Html.Raw().
The existing code in .NET Framework is using an extension method on Controller to get the contents of the file:
public static class ControllerExtension
{
public static string GetContents(this Controller controller, string path)
{
var filepath = controller.Server.MapPath(path);
var contents = System.IO.File.ReadAllText(filepath);
return contents;
}
}
This, of course, does not work in aspnet core. There is no Server.MapPath().
Googling around I found that the recommended approach is to use IWebHostEnvironment. I could do that - inject it into my controller, and pass it to my GetContents() extension, but that's starting to have a bit of a whiff about it.
So I'm wondering, is there some other mechanism for doing the basic task?
Assume I have a number of files in a directory under wwwroot, each containing plain html.
What would be the cleanest way to have a controller include the contents of one of these files in a .cshtml view?
I've used the WebRootPath property of the environment to get to wwwroot and use relative paths from there. Example:
public class MyController
{
private readonly IWebHostEnvironment environment;
public MyController(IWebHostEnvironment environment)
{
this.environment = environment ?? throw new ArgumentNullException(nameof(environment));
}
public IActionResult GetContents(string path)
{
var contentPath = Path.Combine(environment.WebRootPath, path);
var content = System.IO.File.ReadAllText(contentPath);
// Do something with content and return
}
}
You would of course want more checks to validate the user-provided path before attempting to read the file. Also, would likely make this async unless you cache the content and normally serve the memory-cached data.

In ASP.NET 5, how do I get the chosen route in middleware?

I am building an ASP.NET 5 (vNext) site that will host dynamic pages, static content, and a REST Web API. I have found examples of how to create middleware using the new ASP.NET way of doing things but I hit a snag.
I am trying write my own authentication middleware. I would like to create a custom attribute to attach to the controller actions (or whole controllers) that specifies that it requires authentication. Then during a request, in my middleware, I would like to cross reference the list of actions that require authentication with the action that applies to this current request. It is my understanding that I configure my middleware before the MVC middleware so that it is called first in the pipeline. I need to do this so the authentication is done before the request is handled by the MVC controller so that I can't prevent the controller from ever being called if necessary. But doesn't this also mean that the MVC router hasn't determined my route yet? It appears to me the determination of the route and the execution of that routes action happen at one step in the pipeline right?
If I want to be able to determine if a request matches a controller's action in a middleware pipeline step that happens before the request is handled by the controller, am I going to have to write my own url parser to figure that out? Is there some way to get at the routing data for the request before it is actually handled by the controller?
Edit: I'm beginning to think that the RouterMiddleware might be the answer I'm looking for. I'm assuming I can figure out how to have my router pick up the same routes that the standard MVC router is using (I use attribute routing) and have my router (really authenticator) mark the request as not handled when it succeeds authentication so that the default mvc router does the actual request handling. I really don't want to fully implement all of what the MVC middleware is doing. Working on trying to figure it out. RouterMiddleware kind of shows me what I need to do I think.
Edit 2: Here is a template for the middleware in ASP.NET 5
public class TokenAuthentication
{
private readonly RequestDelegate _next;
public TokenAuthentication(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
//do stuff here
//let next thing in the pipeline go
await _next(context);
//do exit code
}
}
I ended up looking through the ASP.NET source code (because it is open source now!) and found that I could copy the UseMvc extension method from this class and swap out the default handler for my own.
public static class TokenAuthenticationExtensions
{
public static IApplicationBuilder UseTokenAuthentication(this IApplicationBuilder app, Action<IRouteBuilder> configureRoutes)
{
var routes = new RouteBuilder
{
DefaultHandler = new TokenRouteHandler(),
ServiceProvider = app.ApplicationServices
};
configureRoutes(routes);
routes.Routes.Insert(0, AttributeRouting.CreateAttributeMegaRoute(
routes.DefaultHandler,
app.ApplicationServices));
return app.UseRouter(routes.Build());
}
}
Then you create your own version of this class. In my case I don't actually want to invoke the actions. I will let the typical Mvc middleware do that. Since that is the case I gut all the related code and kept just what I needed to get the route data which is in actionDescriptor variable. I probably can remove the code dealing with backing up the route data since I dont think what I will be doing will affect the data, but I have kept it in the example. This is the skeleton of what I will start with based on the mvc route handler.
public class TokenRouteHandler : IRouter
{
private IActionSelector _actionSelector;
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
EnsureServices(context.Context);
context.IsBound = _actionSelector.HasValidAction(context);
return null;
}
public async Task RouteAsync(RouteContext context)
{
var services = context.HttpContext.RequestServices;
EnsureServices(context.HttpContext);
var actionDescriptor = await _actionSelector.SelectAsync(context);
if (actionDescriptor == null)
{
return;
}
var oldRouteData = context.RouteData;
var newRouteData = new RouteData(oldRouteData);
if (actionDescriptor.RouteValueDefaults != null)
{
foreach (var kvp in actionDescriptor.RouteValueDefaults)
{
if (!newRouteData.Values.ContainsKey(kvp.Key))
{
newRouteData.Values.Add(kvp.Key, kvp.Value);
}
}
}
try
{
context.RouteData = newRouteData;
//Authentication code will go here <-----------
var authenticated = true;
if (!authenticated)
{
context.IsHandled = true;
}
}
finally
{
if (!context.IsHandled)
{
context.RouteData = oldRouteData;
}
}
}
private void EnsureServices(HttpContext context)
{
if (_actionSelector == null)
{
_actionSelector = context.RequestServices.GetRequiredService<IActionSelector>();
}
}
}
And finally, in the Startup.cs file's Configure method at the end of the pipeline I have it setup so that I use the same routing setup (I use attribute routing) for the both my token authentication and mvc router.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
//Other middleware delcartions here <----------------
Action<IRouteBuilder> routeBuilder = routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
};
app.UseTokenAuthentication(routeBuilder);
//Middleware after this point will be blocked if authentication fails by having the TokenRouteHandler setting context.IsHandled to true
app.UseMvc(routeBuilder);
}
Edit 1:
I should also note that at the moment I am not concerned about the extra time required to select the route twice which is what I think would happen here since both my middleware and the Mvc middleware will be doing that. If that becomes a performance problem then I will build the mvc and authentication in to one handler. That would be best idea performance-wise, but what I have shown here is the most modular approach I think.
Edit 2:
In the end to get the information I needed I had to cast the ActionDescriptor to a ControllerActionDescriptor. I am not sure what other types of actions you can have in ASP.NET but I am pretty sure all my action descriptors should be ControllerActionDescriptors. Maybe the old legacy Web Api stuff needs another type of ActionDescriptor.

How do I process a request for a web page of a pre-compiled web application which is referenced in another web application?

Our company would like to give a pre-compiled version of our web application to a 3rd party so they can add their own pages and modules to it.
In trying to accomplish this, I've so far done the following:
Compiled our main web app as a Web Deployment Project
Created a POC web app which references the DLL resulting from step 1 above.
I then added the following static method to our main web app, which should hopefully process requests to its pre-compiled aspx pages:
public static bool TryProcessRequest(HttpContext context)
{
string rawUrl = context.Request.RawUrl;
int aspxIdx = rawUrl.IndexOf(".aspx");
if (aspxIdx > 0)
{
string aspxPagePath = rawUrl.Substring(0, aspxIdx + 5);
string aspxPageClassName = aspxPagePath.Substring(1).Replace('/','_').Replace(".aspx","");
Assembly website = Assembly.GetAssembly(typeof(MCLLogin));
Type pageClass = website.GetType(aspxPageClassName);
ConstructorInfo ctor = pageClass.GetConstructor(new Type[] { });
IHttpHandler pageObj = (IHttpHandler)ctor.Invoke(new object[] { });
context.Server.Execute(pageObj, context.Response.Output, false);
//alternative: invoking the page's ProcessRequest method - same results
//System.Reflection.MethodInfo method = pageClass.GetMethod("ProcessRequest");
//method.Invoke(pageObj, new object[] { context });
return true;
}
return false; //not handled
}
I am then calling this method in the ProcessRequest() method of a HttpHandler of the POC web app whenever I want our main web app to handle the request. This code indeed successfully instantiates a page of the correct class and starts to process the request.
The problem:
Code in my Page_PreLoad handler throws an exception because Page.Form is null. I've also found out the Page.Controls collection is empty.
What am I doing wrong? Should I go down a different path to achieve this?

ASP .NET MVC VirtualPathProvider

I am writing a VirtualPathProvider to dynamically load my MVC views, which are located in a different directory. I successfully intercept the call before MVC (in FileExists), but in my VirtualPathProvider, I get the raw, pre-routed url like:
~/Apps/Administration/Account/LogOn
Personally, I know that MVC will look for
~/Apps/Administration/Views/Account/LogOn.aspx
and that I should be reading the file contents from
D:\SomeOtherNonWebRootDirectory\Apps\Administration\Views\Account\LogOn.aspx
but I'd rather not hard code the logic to "add the directory named Views and add aspx to the end".
Where is this logic stored and how can I get it into my virtual path provider?
Thanks. Sorry if I'm not being clear.
Edited
You need to make a class that inherits WebFormViewEngine and sets the ViewLocationFormats property (inherited from VirtualPathProviderViewEngine).
The default values can be found in the MVC source code:
public WebFormViewEngine() {
MasterLocationFormats = new[] {
"~/Views/{1}/{0}.master",
"~/Views/Shared/{0}.master"
};
AreaMasterLocationFormats = new[] {
"~/Areas/{2}/Views/{1}/{0}.master",
"~/Areas/{2}/Views/Shared/{0}.master",
};
ViewLocationFormats = new[] {
"~/Views/{1}/{0}.aspx",
"~/Views/{1}/{0}.ascx",
"~/Views/Shared/{0}.aspx",
"~/Views/Shared/{0}.ascx"
};
AreaViewLocationFormats = new[] {
"~/Areas/{2}/Views/{1}/{0}.aspx",
"~/Areas/{2}/Views/{1}/{0}.ascx",
"~/Areas/{2}/Views/Shared/{0}.aspx",
"~/Areas/{2}/Views/Shared/{0}.ascx",
};
PartialViewLocationFormats = ViewLocationFormats;
AreaPartialViewLocationFormats = AreaViewLocationFormats;
}
You should then clear the ViewEngines.Engines collection and add your ViewEngine instance to it.
As SLaks mentioned above, you need to create a Custom View Engine and add your view-finding logic in the FindView method.
public class CustomViewEngine : VirtualPathProviderViewEngine
{
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
//Set view path
string viewPath = GetCurrentViewPath();
//Set master path (if need be)
string masterPath = GetCurrentMasterPath();
return base.FindView(controllerContext, viewPath, masterPath, useCache);
}
}
In the Application_Start, you can register your View Engine like this:
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new CustomViewEngine());
The answer was that MVC was not finding my controller properly. If MVC does in fact find your controller properly, there should be two requests processed by the VirtualPathProvider:
An initial request with the acutal url requested (ie. http://.../Account/LogOn).
A subsequent FileExists check for http://.../Views/Account/LogOn.aspx, after the request in 1. returns false calling FileExists. This actually retuns the aspx content.

Can I use HttpHandler to fake the existence of aspx pages?

I am building a web site with ASP.NET 3.5, and most of the site structure is static enough to create a folder structure and aspx pages. However, the site administrators want the ability to add new pages to different sections of the site through a web interface and using a WYSIWYG editor. I am using nested master pages to give the different sections of the site their own menus. What I would like to do is have a generic page under each section of the site that uses the appropriate master page and has a place holder for content that could be loaded from a database. I would also like these "fake" pages to have a url like any other aspx page, as if they had corresponding files on the server. So rather than have my url be:
http://mysite.com/subsection/gerenicconent.aspx?contentid=1234
it would be something like:
http://mysite.com/subsection/somethingmeaningful.aspx
The problem is that somethingmeaningful.aspx does not exist, because the administrator created it through the web UI, and the content is stored in the database. What I'm thinking is that I'll implement an HTTP handler that handles requests for aspx files. In that handler, I'll check to see if the URL that was requested is an actual file or one of my "fake pages". If it is a request for a fake page, I'll re-route the request to the generic content page for the appropriate section, change the query string to request the appropriate data from the database, and rewrite the URL so that it looks to the user as if the fake page really exists. The problem I'm having right now is that I can't figure out how to route the request to the default handler for aspx pages. I tried to instantiate a PageHandlerFactory, but the constuctor is protected internal. Is there any way for me to tell my HttpHandler to call the HttpHandler that would normal be used to process a request? My handler code currently looks like this:
using System.Web;
using System.Web.UI;
namespace HandlerTest
{
public class FakePageHandler : IHttpHandler
{
public bool IsReusable
{
get { return false; }
}
public void ProcessRequest(HttpContext context)
{
if(RequestIsForFakedPage(context))
{
// reroute the request to the generic page and rewrite the URL
PageHandlerFactory factory = new PageHandlerFactory(); // this won't compile because the constructor is protected internal
factory.GetHandler(context, context.Request.RequestType, GetGenericContentPath(context), GetPhysicalApplicationPath(context)).ProcessRequest(context);
}
else
{
// route the request to the default handler for aspx pages
PageHandlerFactory factory = new PageHandlerFactory();
factory.GetHandler(context, context.Request.RequestType, context.Request.Path, context.Request.PhysicalPath).ProcessRequest(context);
}
}
public string RequestForPageIsFaked(HttpContext context)
{
// TODO
}
public string GetGenericContentPath(HttpContext context)
{
// TODO
}
public string GetPhysicalApplicationPath(HttpContext context)
{
// TODO
}
}
}
I still have some work to do to determine if the request is for a real page, and I haven't rewritten any URLs yet, but is something like this possible? Is there another way to create a PageHandlerFactory other than calling its constructor? Is there any way I can route the request up to the "normal" HttpHandler for an aspx page? I'd basically be saying "process this ASPX request as you normally would."
If you are using 3.5, look into using asp.net routing.
http://msdn.microsoft.com/en-us/library/cc668201.aspx
You would be better off using an http module for this, as in this case you can use the RewritePath method to route the request for fake pages, and do nothing for actual pages which will allow them to be processed as normal.
There is a good explanation of this here which also covers the benefits of using IIS 7.0 if that is an option for you.
I've just pulled this off a similar system we've just written.
This method takes care of physical pages and "fake" pages. You'll be able to ascertan how this fits with your fake page schema, I'm sure.
public class AspxHttpHandler : IHttpHandlerFactory
{
#region ~ from IHttpHandlerFactory ~
public IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTranslated)
{
string url=context.Request.Url.AbsolutePath;
string[] portions = url.Split(new char[] { '/', '\\' });
// gives you the path, i presume this will help you identify the section and page
string serverSidePage=Path.Combine(context.Server.MapPath("~"),url);
if (File.Exists(serverSidePage))
{
// page is real
string virtualPath = context.Request.Url.AbsolutePath;
string inputFile = context.Server.MapPath(virtualPath);
try
{
// if it's real, send in the details to the ASPX compiler
return PageParser.GetCompiledPageInstance(virtualPath, inputFile, context);
}
catch (Exception ex)
{
throw new ApplicationException("Failed to render physical page", ex);
}
}
else
{
// page is fake
// need to identify a page that exists which you can use to compile against
// here, it is CMSTaregtPage - it can use a Master
string inputFile = context.Server.MapPath("~/CMSTargetPage.aspx");
string virtualPath = "~/CMSTargetPage.aspx";
// you can also add things that the page can access vai the Context.Items collection
context.Items.Add("DataItem","123");
return PageParser.GetCompiledPageInstance(virtualPath, inputFile, context);
}
public void ReleaseHandler(IHttpHandler handler)
{
}

Resources