How to implement custom route in ASP.NET MVC containing dot? - asp.net-mvc-4

Instead of the conventional MVC route of
{controller}/{action}/{id}
I need my system to handle requests like this
{controller}/{action}.{id}
e.g.
http://localhost:50691/epws/openWebViewer.x551
I have no control over the system calling me
I modified my routeconfig as such
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}.{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
with the following controller
public class epwsController : Controller
{
public ActionResult openWebViewer(string id)
{
return View(id);
}
}
but the URL above just returns a 404

You could do something like below, remove action parameter from url with route attributes. Then extend id parameter, so string "openWebViewer.x551" gets inside of the action as a whole and do some inner routing inside controller. Finally add web.config setting to add trailing slash to URL so it does not get handled by static file handler
Controller
using System;
using System.Web.Mvc;
namespace WebApplication8.Controllers
{
[RoutePrefix("epws")]
public class EpwsController : Controller
{
[Route("{id}")]
public ActionResult Index(string id)
{
var parameters = id.Split('.');
switch (parameters[0])
{
case "openWebViewer":
return openWebViewer(parameters[1]);
default:
throw new NotImplementedException();
}
}
public ActionResult openWebViewer(string id)
{
return View(id);
}
}
}
RouteConfig.cs
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapMvcAttributeRoutes();
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
Web.config
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="Add trailing slash for some URLs" stopProcessing="true">
<match url="^(.*(\.).+[^\/])$" />
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
</conditions>
<action type="Redirect" url="{R:1}/" />
</rule>
</rules>
</rewrite>
</system.webServer>
I believe static files will be broken with this web.config, but you could adjust regex to match only specific parameter names, e.g. openWebViewer

Related

404 (Not Found) for PUT and EDIT after enabling Cors

I'm using OWIN authentication and testing my Web APIs locally and I got some errors about CORS. I solved the problem just by adding customeHeaders tag in web.config. Now I'm able to login and and also get data. But when I try to PUT or DELETE requests, I get 404 (Not Found) error.
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
}
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Formatters.Remove(config.Formatters.XmlFormatter);
var jsonFormatter = new JsonMediaTypeFormatter();
config.Services.Replace(typeof(IContentNegotiator), new JsonContentNegotiator(jsonFormatter));
}
}
and in web.config
<httpProtocol>
<customHeaders>
<add name="Access-Control-Allow-Origin" value="*" />
<add name="Access-Control-Allow-Methods" value="*" />
<add name="Access-Control-Allow-Headers" value="*" />
</customHeaders>
</httpProtocol>

Correct way to use Serilog with WebApi2

I am in search of the correct way to use serilog with aspnet webapi2.
As for now I initialize the global Log.Logger property like that :
public static void Register(HttpConfiguration config)
{
Log.Logger = new LoggerConfiguration()
.WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://localhost:9200"))
{
IndexFormat = IndexFormat,
BufferBaseFilename = outputLogPath,
AutoRegisterTemplate = true,
AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv6,
CustomFormatter = new ElasticsearchJsonFormatter(renderMessageTemplate: false),
BufferFileCountLimit = NbDaysRetention
})
.MinimumLevel.ControlledBy(new LoggingLevelSwitch() { MinimumLevel = LogEventLevel.Information})
.Enrich.FromLogContext()
.Enrich.WithWebApiRouteTemplate()
.Enrich.WithWebApiActionName()
.CreateLogger();
//Trace all requests
SerilogWebClassic.Configure(cfg => cfg.LogAtLevel(LogEventLevel.Information));
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
Is there a more cleaner way to do it? I am wondering, if this might be a problem if I have to put some test in place for my controllers.
I have used the following code organization quite a lot for apps with Web API (and/or MVC). (note that it may be a bit outdated, as it is based on old versions of some packages, but you should be able to adapt it without too much work ...)
You will need quite a few packages, that you should be able to guess from the namespaces, but most importantly, install the WebActivatorEx package which provides a way to have code running at different moment of the app lifecycle
Our Global.asax.cs ended up looking like this :
public class WebApiApplication : System.Web.HttpApplication
{
// rely on the fact that AppPreStart is called before Application_Start
private static readonly ILogger Logger = Log.ForContext<WebApiApplication>();
public override void Init()
{
base.Init();
}
protected void Application_Start()
{
// WARNING : Some code runs even before this method ... see AppPreStart
Logger.Debug("In Application_Start");
// Mvc (must be before)
AreaRegistration.RegisterAllAreas();
// Web API
// ... snip ...
// some DependencyInjection config ...
// ... snip ...
// MVC
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
Logger.Information("App started !");
}
protected void Application_End(object sender, EventArgs e)
{
Logger.Debug("In Application_End");
ApplicationShutdownReason shutdownReason = System.Web.Hosting.HostingEnvironment.ShutdownReason;
Logger.Information("App is shutting down (reason = {#shutdownReason})", shutdownReason);
// WARNING : Some code runs AFTER Application_End ... see AppPostShutDown
}
}
and then several classes under the App_Start folder (some of which you can just ignore :P ) :
AppPreStart.cs
using XXX;
using Serilog;
[assembly: WebActivatorEx.PreApplicationStartMethod(typeof(AppPreStart), nameof(AppPreStart.PreApplicationStart))]
namespace XXX
{
/// <summary>
/// This runs even before global.asax Application_Start (see WebActivatorConfig)
/// </summary>
public class AppPreStart
{
public static void PreApplicationStart()
{
LogConfig.Configure();
var logger = Log.ForContext<AppPreStart>();
logger.Information("App is starting ...");
// ... snip ...
// very early things like IoC config, AutoMapper config ...
// ... snip ...
logger.Debug("Done with AppPreStart");
}
}
}
AppPostShutDown.cs
using XXX;
using Serilog;
[assembly: WebActivatorEx.ApplicationShutdownMethod(typeof(AppPostShutDown), nameof(AppPostShutDown.PostApplicationShutDown))]
namespace XXX
{
/// <summary>
/// This runs even before global.asax Application_Start (see WebActivatorConfig)
/// </summary>
public class AppPostShutDown
{
private static ILogger Logger = Log.ForContext<AppPostShutDown>();
public static void PostApplicationShutDown()
{
Logger.Debug("PostApplicationShutDown");
// ... snip ...
// very late things like IoC dispose ....
// ... snip ...
// force flushing the last "not logged" events
Logger.Debug("Closing the logger! ");
Log.CloseAndFlush();
}
}
}
LogConfig.cs
using Serilog;
using Serilog.Events;
using SerilogWeb.Classic;
using SerilogWeb.Classic.Enrichers;
using SerilogWeb.Classic.WebApi.Enrichers;
namespace XXX
{
public class LogConfig
{
static public void Configure()
{
ApplicationLifecycleModule.LogPostedFormData = LogPostedFormDataOption.OnlyOnError;
ApplicationLifecycleModule.FormDataLoggingLevel = LogEventLevel.Debug;
ApplicationLifecycleModule.RequestLoggingLevel = LogEventLevel.Debug;
var loggerConfiguration = new LoggerConfiguration().ReadFrom.AppSettings()
.Enrich.FromLogContext()
.Enrich.With<HttpRequestIdEnricher>()
.Enrich.With<UserNameEnricher>()
.Enrich.With<HttpRequestUrlEnricher>()
.Enrich.With<WebApiRouteTemplateEnricher>()
.Enrich.With<WebApiControllerNameEnricher>()
.Enrich.With<WebApiRouteDataEnricher>()
.Enrich.With<WebApiActionNameEnricher>()
;
Log.Logger = loggerConfiguration.CreateLogger();
}
}
}
and then read the variable parts of the Log configuration from Web.config that has the following keys in AppSettings :
<!-- SeriLog-->
<add key="serilog:level-switch:$controlSwitch" value="Information" />
<add key="serilog:minimum-level:controlled-by" value="$controlSwitch" />
<add key="serilog:enrich:with-property:AppName" value="XXXApp" />
<add key="serilog:enrich:with-property:AppComponent" value="XXXComponent" />
<add key="serilog:enrich:with-property:Environment" value="Dev" />
<add key="serilog:enrich:with-property:MachineName" value="%COMPUTERNAME%" />
<add key="serilog:using:Seq" value="Serilog.Sinks.Seq" />
<add key="serilog:write-to:Seq.serverUrl" value="http://localhost:5341" />
<add key="serilog:write-to:Seq.apiKey" value="xxxxxxxxxxx" />
<add key="serilog:write-to:Seq.controlLevelSwitch" value="$controlSwitch" />
(and then we had Web.config transforms to turn it into a "tokenized" file for production
Web.Release.config
<!-- SeriLog-->
<add key="serilog:enrich:with-property:Environment" value="__Release_EnvironmentName__"
xdt:Transform="Replace" xdt:Locator="Match(key)"/>
<add key="serilog:write-to:Seq.serverUrl" value="__app_serilogSeqUrl__"
xdt:Transform="Replace" xdt:Locator="Match(key)"/>
<add key="serilog:write-to:Seq.apiKey" value="__app_serilogApiKey__"
xdt:Transform="Replace" xdt:Locator="Match(key)"/>
Some of the most important parts of this configuration is :
configure your logger as soon as possible
call Log.CloseAndFlush(); at the very end to be sure all your log events are stored/pushed
add Enrich.FromLogContext() from Serilog, and some enrichers from SerilogWeb.Classic and SerilogWeb.WebApi, they can turn out to be super useful.
logging to a log server that properly supports structured logging (writing to files just has too many drawbacks) ... we used Seq and were very very happy about it (installed locally on every dev machine, and then a centralized instance in production). It supports searching/querying and dashboards and also dynamic log level control.

The resource cannot be found using Area as separate project

I want to use area as separate project.Whenever I try to navigate to this link :
http://localhost:100/module/home
I get the following error:
My solution contains two projects : entity_framework and module
My solution looks like:
module is the project that I wanted to use as an area.My main project:
My Route.config in entity_framework looks :
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "User", action = "Index", id = UrlParameter.Optional },
namespaces:new string[] {"entity_framework.Controllers"}
);
}
}
My moduleAreaRegistration.cs :
namespace entity_framework
{
public class moduleAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "module";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"module_default",
"module/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional },
new string[] {"entity_framework.Controllers"}
);
}
}
}
I have added Index action in home controller in both projects and Index view too but I am stuck in this error. Thanks in advance.
I changed the namespace to
new string[] { "module.Controllers" }
in moduleAreaRegistration.cs .
Then I added this inside web.config inside main project:
<appSettings>
<add key="owin:AutomaticAppStartup" value="false" />
</appSettings>
That worked fine for me.

Portable Area not finding embedded files MVC4 C#

I have a solution with two projects in it. A portable areas project, and a web site that references the portable areas project, and Both reference MvcContrib.
The problem I am have is with the embedded resources, they are giving me a 404 error when I try to get to them. It seems like it's trying to access a physical path not the dll. The partial view works fine.
The file I'm trying to access looks like this inside my visual studio solution explorer
AdHocReporting/Areas/AdHocReportBuilder/Content/adhoc.css (the build action is embedded)
Here is the routing for the portable area:
using System.Web.Mvc;
using MvcContrib.PortableAreas;
namespace AdHocReporting.Areas.AdHocReportBuilder
{
public class AdHocReportBuilderAreaRegistration : PortableAreaRegistration
{
public override string AreaName
{
get { return "AdHocReportBuilder"; }
}
public override void RegisterArea(AreaRegistrationContext context, IApplicationBus bus)
{
RegisterRoutes(context);
RegisterAreaEmbeddedResources();
}
private void RegisterRoutes(AreaRegistrationContext context)
{
context.MapRoute(
AreaName + "_content",
base.AreaRoutePrefix + "/Content/{resourceName}",
new { controller = "EmbeddedResource", action = "Index", resourcePath = "Content" },
new[] { "MvcContrib.PortableAreas" }
);
context.MapRoute(
AreaName + "_default",
base.AreaRoutePrefix + "/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
}
}
Here is the Web sites Global.aspx that has the reference to the portable area
namespace AdHocReportingSite
{
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_Start()
{
AreaRegistration.RegisterAllAreas();
PortableAreaRegistration.RegisterEmbeddedViewEngine();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
}
}
}
here is an image of what my solution explore looks like:
Could not reproduce the issue once I created a new project and redid everything. I'm not sure if it was an error or I just missed a step in setting up my portable areas.
In the Web.Config add for each file type:
<system.webServer>
<handlers>
<add name="js" path="*.js" verb="*" type="System.Web.Handlers.TransferRequestHandler" resourceType="File" preCondition="integratedMode" />
</handlers>
</system.webServer>
This will make IIS try to use the defined routes instead of searching for the static file.

create filter to ensure lowercase urls

I have the following code in the global.asax of my mvc web application:
/// <summary>
/// Handles the BeginRequest event of the Application control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
protected void Application_BeginRequest(object sender, EventArgs e)
{
// ensure that all url's are of the lowercase nature for seo
string url = Request.Url.ToString();
if (Request.HttpMethod == "GET" && Regex.Match(url, "[A-Z]").Success)
{
Response.RedirectPermanent(url.ToLower(CultureInfo.CurrentCulture), true);
}
}
What this achieves is to ensure all url's accessing the site are in lower case. I would like to follow the MVC pattern and move this to a filter that can applied globally to all filters.
Is this the correct approach? And how would I create a filter for the above code?
My opinion - a filter is too late to handle a global url rewrite. But to answer your question as to how, create an action filter:
public class LowerCaseFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// ensure that all url's are of the lowercase nature for seo
var request = filterContext.HttpContext.Request;
var url = request.Url.ToString();
if (request.HttpMethod == "GET" && Regex.Match(url, "[A-Z]").Success)
{
filterContext.Result = new RedirectResult(url.ToLower(CultureInfo.CurrentCulture), true);
}
}
}
and in FilterConfig.cs, register your global filter:
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
filters.Add(new LowerCaseFilterAttribute());
}
}
HOWEVER, I would recommend pushing this task out to IIS and using a rewrite rule. Make sure the URL Rewrite Module is added to IIS and then add the following rewrite rule in your web.config:
<!-- http://ruslany.net/2009/04/10-url-rewriting-tips-and-tricks/ -->
<rule name="Convert to lower case" stopProcessing="true">
<match url=".*[A-Z].*" ignoreCase="false" />
<conditions>
<add input="{REQUEST_METHOD}" matchType="Pattern" pattern="GET" ignoreCase="false" />
</conditions>
<action type="Redirect" url="{ToLower:{R:0}}" redirectType="Permanent" />
</rule>