Creating a failover route in MVC 5 - asp.net-mvc-routing

I would like it if any Url that is not found is sent to my "Home" controller. And I mean like total garbage urls passed in Like SomeDomain/ThisIsNotAController/Goober/adsfasdf1?spacerocket.
A 404 error is just confusing to many people.
How do I map a route to say Home/Index if all others fail. I know you can do this in Rails.
Thanks in advance. and yes I did search for this, but I guess I don't know what to ask for
TW

You could use custom errors in the web.config file for this purpose by specifying an action method in the controller you wish...
public ActionResult NotFound(string aspxerrorpath)
{
LogManager.Current.LogError(aspxerrorpath);
Response.StatusCode = 404;
return View();
}
Create the view as you normally would and then add the custom errors element in the web.config file...
<customErrors allowNestedErrors="true" mode="On">
<error statusCode="404" redirect="~/Your_Controller/NotFound"/>
</customErrors>

Related

docs works but ui doesn't have routing?

I'm running my api as an application within a site in IIS. The site's url is "http://api.companyName.local/" and the api's url is "http://api.companyName.local/api".
I've got Swagger-Net set up enough that http://api.companyName.local/api/swagger/docs/v1 returns swagger.json and appears to be correct. However no url I can think of brings up the UI. I've tried:
http://api.companyName.local/api/swagger
http://api.companyName.local/api/swagger/
http://api.companyName.local/api/swagger/ui
http://api.companyName.local/api/swagger/ui/
http://api.companyName.local/api/swagger/ui/index
all of which end up with this error:
It looks like the routes are resolving to the StaticFile handler and not the SwaggerUiHandler handler.
This is the setup I'm trying to use
httpConfiguration.EnableSwagger( c => {
c.SingleApiVersion( "v1", "Company Name API" );
c.ResolveConflictingActions( apiDescriptions => apiDescriptions.First( ) );
c.UseFullTypeNameInSchemaIds( );
} ).EnableSwaggerUi( );
I've pulled down the github for Swagger-Net and started to dig around. I see it uses httpConfiguration.VirtualPathRoot in some places so I figure I should mention for my api this resolves to "/" in case that matters.
EDIT:
One interesting thing I just found is that I get different errors from these two urls
http://api.companyName.local/api/swagger/ui/v1
http://api.companyName.local/api/swagger/ui/v2
The one that ends in v1 is getting into the SwaggerUiHandler but the one with v2 errors like it would without v2 at all. So it would seem that something in Swagger-Net is looking at the full url and ignoring it if it doesn't include the version. My api doesn't have versions so this is very annoying.
I finally figured it out! The solution is to add a TransferRequestHandler to the web.config to allow the ui pages to resolve into the api. The reason routes with v1 worked is because my api already had this attribute for routes with v1 in them.
<handlers>
<add name="Swashbuckle-Swagger" verb="*" path="swagger/*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0"/>
</handlers>
This snipped was lifted from https://github.com/domaindrivendev/Swashbuckle/issues/57

Grails - RESTful Web service responds with 404 when controller is defined

I defined a domain model class with a few properties and marked it as a RESTful resource using #Resource following the official Grails guide on Web services. Now, when testing the application (using Ruby's RestClient) I can see that things are working fine. However, after defining an associated Controller that overrides save method (for creating new resource), I've been getting 404 even on just simple GET requests. I defined some test objects using BootStrap so GET should be working.
My controller code looks like this:
class ModelController {
#Transactional
def save(Model model) {
def status = 201
if (model.validate()) {
model.save(flush: true, failOnError: true)
} else {
status = 422
}
render status:status
}
}
Do I still need to do something with the UrlMappings.groovy or is there something wrong with my controller code (all my unit tests for it are passing though)?
Update
I have created a sample project to demonstrate what's happening. Please check my bookstore-demo repository on GitHub. In the repository, I've defined 2 tags, rest-working, and rest-not-working. The first one marks the point where things are still working, and second one, as the name suggests, marks where I've created a controller that causes 404 response. This is pretty much what I've done with my actual project so far, and I'm getting the the same error.
The code looks OK, if you are getting a 404 then it sounds like its not even hitting this Controller. I would open developer console and try capture what URL it is hitting - URL being sent is potentially incorrect. If I am experimenting I tend to put println "1" println "2" and so on between my logics to see where the code is going or did it return it at all meaning it didn't even get there. so maybe if you doubt your code try this tactic between your if statements see which numbers get hit.
Also there is not a lot to go on to try give anything of more useful as feedback, but I would also check out the allowedMethods of this Controller if any defined...
unsure how it is being posted by if you have ..
static allowedMethods = [save: "POST", update: "POST", delete: "POST"]
try changing it to
static allowedMethods = [update: "POST", delete: "POST"]
unsure if this is still supported:
static allowedMethods = [save:['POST','GET'],update: "POST", delete: "POST"]
For all the methods refer to:
http://grails.org/doc/latest/ref/Controllers/allowedMethods.html

Creating a REST service in Sitecore

I'm trying to build a REST service in a Sitecore root. My application start looks like this:
void Application_Start(object sender, EventArgs e)
{
RouteTable.Routes.MapHttpRoute(
name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = System.Web.Http.RouteParameter.Optional });
}
And my URL looks like this:
http://{mydomain}/api/books
I have the correct controller and all that.
But Sitecore keeps redirecting me to the 404 page. I've added the path to the IgnoreUrlPrefixes node in the web.config, but to no avail. If I had to guess, I'd think that Sitecore's handler is redirecting before my code gets the chance to execute, but I really don't know.
Does anybody have any idea what might be wrong?
Your assessment is correct. You need a processor in the httpRequestBegin pipeline to abort Sitecore's processing. See the SystemWebRoutingResolver in this answer:
Sitecore and ASP.net MVC
It's also described in this article:
http://www.sitecore.net/Community/Technical-Blogs/John-West-Sitecore-Blog/Posts/2010/10/Sitecore-MVC-Crash-Course.aspx
But I'll include the code here as well. :)
public class SystemWebRoutingResolver : Sitecore.Pipelines.HttpRequest.HttpRequestProcessor
{
public override void Process(Sitecore.Pipelines.HttpRequest.HttpRequestArgs args)
{
RouteData routeData = RouteTable.Routes.GetRouteData(new HttpContextWrapper(args.Context));
if (routeData != null)
{
args.AbortPipeline();
}
}
}
Then in your httpRequestBegin configuration:
<processor type="My.SystemWebRoutingResolver, My.Classes" />
You might want to have a look at Sitecore Web Api
It's pretty much the same you are building.
Another option, which I've used to good effect, is to use the content tree, the "star" item, and a sublayout/layout combination dedicated to this purpose:
[siteroot]/API/*/*/*/*/*/*/*/*/*
The above path allows you to have anywhere between 1 and 9 segments - if you need more than that, you probably need to rethink your process, IMO. This also retains all of the Sitecore context. Sitecore, when unable to find an item in a folder, attempts to look for the catch-all star item and if present, it renders that item instead of returning a 404.
There are a few ways to go about doing the restful methods and the sublayout (or sublayouts if you want to segregate them by depth to simplify parsing).
You can choose to follow the general "standard" and use GET, PUT, and POST calls to interact with these items, but then you can't use Sitecore Caching without custom backend caching code). Alternately, you can split your API into three different trees:
[siteroot]/API/GET/*/*/*/*/*/*/*/*/*
[siteroot]/API/PUT/*/*/*/*/*/*/*/*/*
[siteroot]/API/POST/*/*/*/*/*/*/*/*/*
This allows caching the GET requests (since GET requests should only retrieve data, not update it). Be sure to use the proper caching scheme, essentially this should cache based on every permutation of the data, user, etc., if you intend to use this in any of those contexts.
If you are going to create multiple sublayouts, I recommend creating a base class that handles general methods for GET, PUT, and POST, and then use those classes as the base for your sublayouts.
In your sublayouts, you simply get the Request object, get the path (and query if you're using queries), split it, and perform your switch case logic just as you would with standard routing. For PUT, use Response.ReadBinary(). For POST use the Request.Form object to get all of the form elements and iterate through them to process the information provided (it may be easiest to put all of your form data into a single JSON object, encapsulated as a string (so .NET sees it as a string and therefore one single property) and then you only have one element in the post to deserialize depending on the POST path the user specified.
Complicated? Yes. Works? Yes. Recommended? Well... if you're in a shared environment (multiple sites) and you don't want this processing happening for EVERY site in the pipeline processor, then this solution works. If you have access to using MVC with Sitecore or have no issues altering the pipeline processor, then that is likely more efficient.
One benefit to the content based method is that the context lifecycle is exactly the same as a standard Sitecore page (logins, etc.), so you've got all the same controls as any other item would provide at that point in the lifecycle. The negative to this is that you have to deal with the entire page lifecycle load before it gets to your code... the pipeline processor can skip a lot of Sitecore's process and just get the data you need directly, making it faster.
you need to have a Pipeline initializer for Routing:
It will be like :
public class Initializer
{
public void Process(PipelineArgs args)
{
RouteCollection route = RouteTable.Routes;
route.MapHttpRoute("DefaultApi", "api/{controller}/{action}/{id}",
new { id = RouteParameter.Optional });
}
}
On config file you will have :
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<pipelines>
<initialize>
<processor type="_YourNameSpace.Initializer,_YourAssembly" />
</initialize>
</pipelines>
</sitecore>
</configuration>
Happy coding

Forms LoginUrl - how do I format this to work with an area?

I have added an Area to my Web project wherein I hope to perform all the necessary authentication tasks. The folder structure in the project is:
- Solution
- Web Project
- Areas
- Accounts
- Controllers
AccountController.cs
In the AccountController is the usual LogOn(LogOnModel model, string returnUrl) Action.
In the AccountsAreaRegistration.cs is the auto-generated route:
context.MapRoute(
"Accounts_default",
"Accounts/{controller}/{action}/{id}",
new {
action = "LogOn",
id= UrlParameter.Optional
}
);
The first thing that occurred to me was to simply add the name of the area to the loginUrl attribute in the web.config, and let the route mapping take care of the redirect - pretty simple, I thought:
<forms loginUrl="~/Accounts/Account/LogOn"
timeout="2880" />
The only problem is that it doesn't work. I get an error saying "unable to find "/Accounts/Account/Logon" - or whatever value I put in the loginUrl attribute. It seems to me that the URL I specify in the Web.Config isn't pushing that value through the route table to look for a match.
*note: all of this is being triggered by an [Authorize] attribute on an action in one of my other controllers.
EDIT
The workaround I have found is to leave the URL as the default ("Account/Logon") and add another route in the global.asax to redirect the request to the right area:
routes.MapRoute("LogOn",
"Account/LogOn/{id}",
new { controller = "Account",
action = "LogOn",
id = UrlParameter.Optional}
).DataTokens.Add("area", "Accounts");
This gets the job done, but I don't know if it's the best solution to the problem.
Is the controller action you're testing tagged with the [Authorize] attribute?
Which part isn't working, the authorization of some action or the routing to the logon action after requesting an authorized action?
The workaround I have found is to leave the URL as the default ("Account/Logon") and add another route in the global.asax to redirect the request to the right area:
routes.MapRoute("LogOn",
"Account/LogOn/{id}",
new { controller = "Account",
action = "LogOn",
id = UrlParameter.Optional}
).DataTokens.Add("area", "Accounts");
This gets the job done, but I don't know if it's the best solution to the problem.

TempData["message"] isn't reliable-- what am I doing wrong?

I'm using TempDate["Message"] to show little update banners as the user does things on my site like this:
[AcceptVerbs(HttpVerbs.Post), Authorize(Roles = "Admins")]
public ActionResult Delete(int id)
{
_Repo.DeletePage(id); // soft-delete
TempData["Message"] = "Page deleted!";
return RedirectToAction("Revisions", "Page", new { id = id });
}
Then in my master page I have this:
<%-- message box (show it only if it contains a message) --%>
<% string Message = (TempData["Message"] ?? ViewData["Message"]) as string;
if(!string.IsNullOrEmpty(Message)){
%>
<div id="message"><%:Message %></div>
<% }
TempData["Message"] = null; ViewData["Message"] = null; %>
I hit both TempData and ViewData because I read somewhere that TempData should be used for redirects and ViewData should be used otherwise.
The issue is: often the message won't show up right away. Sometimes it takes a click or two to different parts of the site for the message to show up. It's very strange.
Any ideas?
You should verify all places where you use TempData["Message"] in your code. Corresponds to ASP.NET MVC does browser refresh make TempData useless? you can read TempData["Message"] only once (see also http://forums.asp.net/p/1528070/3694325.aspx). During the first uage of TempData["Message"], the TempData["Message"] will be deleted from the internal TempDataDictionary.
Probably it would be better to use TempData["Message"] only inside of Revisions action of the Page controller and not inside of master page or inside a View.
TempData is not intended to pass data to views, hence the name ViewData for that purpose. In fact, I can't think of a reason to use TempData from within a view definition at all...
One very common usage of TempData is the passing of information between controller actions when you do a redirect (the Revisions action in your example above, for instance, would be able to make use of your TempData["Message"] variable).
This is common practice in the PRG means of coding MVC interactions (Post-Redirect-Get) since you often need to pass information from the initial target action when doing the Redirect to the Get. An example of how this might be useful in a Get is below where I often just default to a new viewmodel UNLESS there is one already passed from a redirect in TempData:
public ActionResult System() {
SystemAdminVM model = (SystemAdminVM)TempData["screenData"] ?? new SystemAdminVM();
One more thing; I see you explicitly clearing your TempData and ViewData dictionary entries in your view. You don't need to do that as by that point they are at the end of their life spans anyway...
Happy coding!
Your app's behavior is the one you'd expect if you're using TempData where you should be using ViewData.
You want to double-check that you're storing your status feedbacks in TempData only when the controller does a re-direct. Otherwise, you should use ViewData.
This smells like you need a couple of unit tests to confirm the behavior you're seeing. Try writing up a couple using this example as a starting point:
http://weblogs.asp.net/leftslipper/archive/2008/04/13/mvc-unit-testing-controller-actions-that-use-tempdata.aspx
If you have configured multiple worker process for your application, but session state mode is "InProc", then you can't use default TempData implementation, as session state becomes unusable. (see ASP.NET session state and multiple worker processes)
You could try to use MvcFutures CookieTempDataProvider instead.