ASP.NET webAPI event confusion on hitting a non-existing route - c#

I have a simple ASP.NET webAPI project with just one route. When the route is called in httpModule BeingRequest is fired first followed by EndRequest as expected.
However when i hit the API with a route that doesn't exist i see the following behaivour in my HttpModule:
BeginRequest Called ---> Begin Request Called ---> EndRequest (Response code 404) ---> EndRequest (Response Code 200)
Below is the HttpModule:
using System;
using System.Web;
public class TestModule : IHttpModule
{
public String ModuleName
{
get { return "TestModule"; }
}
public void Init(HttpApplication application)
{
application.BeginRequest += Application_BeginRequest;
application.EndRequest += Application_EndRequest;
}
private void Application_BeginRequest(Object source,
EventArgs e)
{
HttpApplication application = (HttpApplication)source;
HttpContext context = application.Context;
string filePath = context.Request.FilePath;
}
private void Application_EndRequest(Object source, EventArgs e)
{
HttpApplication application = (HttpApplication)source;
HttpContext context = application.Context;
string filePath = context.Request.FilePath;
}
public void Dispose() { }
}
and my controller:
public class SampleController : ApiController
{
[Route("Test")]
[HttpGet]
public IHttpActionResult WriteLog()
{
//omitted code
return Ok(true);
}
}
and my standard webApiConfig:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
Problem is i'm logging the Response fields of each request to a database including the HttpContext.Current.Response.Status, but since the EndRequest event is raised twice i end up with 200 Status code even where the URL\route was not found.
Can someone explain the issue here? is the duplicate firing of BeginRequest an expected behaviour?
UPDATE:
I've observed that this issue (BeginRequest being called twice) only occurs with attribute based routing. It does not happen when I use the conventional routing method:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}"
);
I cannot disallow the usage of attribute based routing so still need to fix the issue. Any pointers?

Here's what i ended up doing to fix my issue where Response Status code of 200 was written to logs even where invalid route was called (404):
private void Application_BeginRequest(object sender, EventArgs eventArgs)
{
if (System.Web.Routing.RouteTable.Routes.GetRouteData(new HttpContextWrapper(HttpContext.Current)) == null)
{
HttpContext.Current.Response.StatusCode = 404;
}
}

Related

Angular / Web API unable to communicate on endpoint

I'm running into an issue where Angular is unable to make a successful request on 2 of 3 endpoints but I have confirmed all work with postman and that the proxy is successfully routing the calls. Chrome - Network tab shows "blocked:other" and output of "ng serve" doesn't indicate anything in regards to proxy. Is there an issue with my angular service and/or server side controller?
Controller
[Route("api/[controller]")]
[ApiController]
public class AffiliationController : ControllerBase
{
public AffiliationController(
IQueryHandler<GetAffiliationsQuery, IQueryable<Affiliation>> getAffiliationsQuery,
ICommandHandler<ToggleAffiliationExclusionCommand> toggleAffiliationExclusionsCommand)
{
_getAffiliationsQuery = getAffiliationsQuery ?? throw new ArgumentNullException(nameof(getAffiliationsQuery));
_toggleAffiliationExclusionsCommand = toggleAffiliationExclusionsCommand ?? throw new ArgumentNullException(nameof(toggleAffiliationExclusionsCommand));
}
private readonly IQueryHandler<GetAffiliationsQuery, IQueryable<Affiliation>> _getAffiliationsQuery;
private readonly ICommandHandler<ToggleAffiliationExclusionCommand> _toggleAffiliationExclusionsCommand;
// Successfully Called
[HttpGet]
public async Task<ActionResult<PaginationResult<Affiliation>>> GetAffiliations([FromQuery] PaginationModel model)
{
var affiliations = await _getAffiliationsQuery.Handle(new GetAffiliationsQuery())
.PaginateAsync(model.PageIndex,
model.PageSize);
return affiliations;
}
// Not reached via Angular
[HttpGet("{id}")]
public async Task<ActionResult<Affiliation>> Foo(int id)
{
var target = await _getAffiliationsQuery.Handle(new GetAffiliationsQuery())
.SingleOrDefaultAsync(afn => afn.Id == id);
if (target == null)
{
return NotFound();
}
await _toggleAffiliationExclusionsCommand.HandleAsync(new ToggleAffiliationExclusionCommand(id));
return target;
}
}
Angular Service
export class AffiliationService {
private readonly apiUrl = 'api/affiliation';
constructor(
private http: HttpClient
) { }
public get(model: PaginationQuery): Observable<PaginationResult<Affiliation>> {
// Works just fine
return this.http
.get<PaginationResult<Affiliation>>(`${this.apiUrl}`, { params: UtilsService.buildQueryParams(model) })
.pipe(
catchError(this.handleError)
);
}
public toggle(affiliation: Affiliation) {
// Does not work unless I pass the 'id' as a query string parameter instead of a route parameter
return this.http
.get(`${this.apiUrl}/${affiliation.id}`)
.pipe(
catchError(this.handleError)
);
}
private handleError(error: HttpErrorResponse) {
if (error.error instanceof ErrorEvent) {
console.error('An error occurred:', error.error.message);
} else {
console.error(`Backend returned code ${error.status}, body was:`);
console.error(error.error);
}
return throwError('Something bad happened; please try again later.');
}
}
proxy.conf.json
{
"/api/*": {
"target": "http://localhost:5000",
"secure": false,
"logLevel": "debug"
}
}
NG Serve Output
Note
Changing the 'toggle' function to the following is able to successfully reach the endpoint. But I don't understand why this works over the other way as I can call the endpoint using the route param approach via Postman
public toggle(affiliation: Affiliation) {
return this.http
.get(`${this.apiUrl}`, { params: UtilsService.buildQueryParams({ id: affiliation.id }) })
.pipe(
catchError(this.handleError)
);
}
This looks like a CORS issue.
If you want to fix this without using a plugin, you can use the WebApi Cors package.
If using a C# ASP.NET CORE project:
Refer to the following page to enable CORS and the request can be
called.
https://learn.microsoft.com/en-us/aspnet/core/security/cors?view=aspnetcore-2.1
If using a C# WebAPI project:
Run the following command to install a CORS package for WebApi:
Install-Package Microsoft.AspNet.WebApi.Cors
Then you can use it as follows:
using System.Web.Http;
namespace WebService
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// New code
config.EnableCors();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
}
and refer to the following URL for the full microsoft guide:
https://learn.microsoft.com/en-us/aspnet/web-api/overview/security/enabling-cross-origin-requests-in-web-api

Web API Self Hosted on windows service using OWIN , Complex Objects always null

I have Web API Project , it is referencing dll of database entities , it contain API Controllers , that return complex objects
when i test the Web API using visual studio (browser) or Telerik Fiddler before hosting the API on windows service , it is working fine and return a json of complex objects as expected .
After i create another project for windows service to self host the wep API using Owin
when i test any controller that return a complex object , it always return null while if i test any controller that return simple string , it is working fine , please help
I don't know why same things don't work after host on windows service
here my windows service startup.cs file
public class Startup
{
public void Configuration(IAppBuilder appBuilder)
{
// Configure Web API for self-host.
HttpConfiguration config = new HttpConfiguration();
var assembly = System.Reflection.Assembly.GetExecutingAssembly().Location;
string path = assembly.Substring(0, assembly.LastIndexOf("\\")) + "\\API.dll";
config.Services.Replace(typeof(IAssembliesResolver), new SelfHostAssemblyResolver(path));
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
appBuilder.UseWebApi(config);
}
}
here is windows service class which inherit from service base class
public partial class selfHosting : ServiceBase
{
public string baseAddress = "http://localhost:9000/";
private IDisposable _server = null;
public selfHosting()
{
InitializeComponent();
}
protected override void OnStart(string[] args)
{
_server = WebApp.Start<Startup>(url: baseAddress);
}
protected override void OnStop()
{
if (_server != null)
{
_server.Dispose();
}
base.OnStop();
}
}
below is WebApiConfig File under another project (API)
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Formatters.Remove(config.Formatters.XmlFormatter);
config.Formatters.JsonFormatter.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented;
config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();
}
}
after i check localhost:9000/api/employee
it return this line
This XML file does not appear to have any style information associated with it. The document tree is shown below.
<ArrayOfEmployees xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/Application.Model" i:nil="true"/>
Controller in web API Project
public class employeeController : ApiController
{
static readonly IRepository<employee> repository = new employeeRepository();
// GET api/<controller>
public IEnumerable<employee> Get()
{
return repository.GetAll();
}
// GET api/<controller>/5
public string Get(int id)
{
return "value";
}
// POST api/<controller>
public void Post([FromBody]string value)
{
}
// PUT api/<controller>/5
public void Put(int id, [FromBody]string value)
{
}
// DELETE api/<controller>/5
public void Delete(int id)
{
}
}
if the get() method return simple string , it is working fine
for example
// GET api/<controller>
public string Get()
{
return "Hello" ;
}

WebApi 2 AssemblyResolver

I have created a controller assembly containing all my controllers in WebApi 2.0 and followed this article - https://www.strathweb.com/2013/08/customizing-controller-discovery-in-asp-net-web-api/ and created a CustomAssemblyResolver and added code to replace the assembly resolver in Application_Start. Here's what my code looks like:
My CustomAssemblyResolver:
public class CustomAssemblyResolver : DefaultAssembliesResolver
{
public override ICollection<Assembly> GetAssemblies()
{
ICollection<Assembly> baseAssemblies = base.GetAssemblies();
List<Assembly> assemblies = new List<Assembly>(baseAssemblies);
string thirdPartySource = "C:\\Research\\ExCoWebApi\\ExternalControllers";
if (!string.IsNullOrWhiteSpace(thirdPartySource))
{
if (Directory.Exists(thirdPartySource))
{
foreach (var file in Directory.GetFiles(thirdPartySource, "*.*", SearchOption.AllDirectories))
{
if (Path.GetExtension(file) == ".dll")
{
var externalAssembly = Assembly.LoadFrom(file);
baseAssemblies.Add(externalAssembly);
}
}
}
}
return baseAssemblies;
}
}
My Application_Start:
protected void Application_Start()
{
Debugger.Launch();
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
GlobalConfiguration.Configuration.Services.Replace(typeof(IAssembliesResolver), new CustomAssemblyResolver());
}
As you can see I'm replacing the default assembly resolver with a CustomAssemblyResolver.
Here's how I'm registering route to the third party controller:
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapHttpRoute(
name: "Test",
routeTemplate: "api/thirdparty/math",
defaults: new { controller = "Math" }
);
}
This is how my MathController looks like that lives in a separate assembly:
public class MathController : ApiController
{
[HttpPost]
public int AddValues(MathRequest request)
{
int response = MathOperations.Add(request.Value1, request.Value2);
return response;
}
}
This is the endpoint I hit via postman: http://localhost/ExCoWebApi/api/thirdparty/math with a POST operation and MathRequest JSON string in the Body and this is what I get as a response:
{
"Message": "No HTTP resource was found that matches the request URI 'http://localhost/ExCoWebApi/api/thirdparty/math'.",
"MessageDetail": "No type was found that matches the controller named 'Math'."
}
After debugging I found that the AssemblyResolver does get replaced with CustomAssemblyResolver at the Application Start but the problem is the GetAssemblies() method of CustomAssemblyResolver doesn't get called. Hence the assembly containing MathController doesn't get loaded.
What am I missing here??
EDIT
In a desperate effort to finding solution to this I stood up a test method in which I build my own HttpConfiguration object and replace the AssemblyResolver in it and it works like a charm! The GetAssemblies() function from the CustomAssemblyResolver gets called and I'm able to call my external controller.. I wonder if there's a difference in the HttpConfiguration object that is returned from "GlobalConfiguration.Configuration" vs. instantiating one manually.. Any help on this would be greatly appreciated. Here's the code for my test method:
[TestMethod]
public void TestExternalControllerCall()
{
try
{
HttpClient client = GetClient();
MathRequest mathReq = new MathRequest { Value1 = 10, Value2 = 20 };
var response = client.PostAsJsonAsync(string.Concat("http://localhost/api/thirdparty/math"), mathReq).Result;
var entResponse = response.Content.ReadAsAsync<int>().Result;
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
}
private HttpClient GetClient()
{
HttpConfiguration config = new HttpConfiguration();
config.Routes.MapHttpRoute(
name: "Test",
routeTemplate: "api/thirdparty/math",
defaults: new { controller = "Math" }
);
config.Services.Replace(typeof(IAssembliesResolver), new CustomAssemblyResolver());
HttpServer server = new HttpServer(config);
HttpClient client = new HttpClient(server);
return client;
}
Found the issue:
The reason why the GetAssemblies() was not getting called is because the callback method (WebApiConfig.Register) set for GlobalConfiguration.Configure which was calling "config.MapHttpAttributeRoutes();". The
GlobalConfiguration.Configuration.Services.Replace(typeof(IAssembliesResolver), new CustomAssemblyResolver());
code actually belongs in the callback method configured in GlobalConfiguration.Configure as opposed to Global.asax if you have a callback set for GlobalConfiguration.Configure. So I moved the IAssemblyResolver replace statement to the Register method in WebApiConfig:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Services.Replace(typeof(IAssembliesResolver), new CustomAssemblyResolver());
// Web API routes
config.MapHttpAttributeRoutes();
}
}

how to add this code on a self hosted api?

According to this question I cannot immplement IHttpModule when self hsoting a web api on a windows service
Is there a way to add an httpModule when webApi is running with the HttpSelfHostServer?
However, I still need to add this code somewhere in my self hosted web api.
I found this blog about how to fix that:
http://www.silver-it.com/node/182
The code is as follows, however I can not have an IhttpModule implemented on aself hosted API
public class CORSPreflightModule : IHttpModule
{
private const string OPTIONSMETHOD = "OPTIONS";
private const string ORIGINHEADER = "ORIGIN";
private const string ALLOWEDORIGIN = "https://yourspodomain.sharepoint.com";
void IHttpModule.Dispose()
{
}
void IHttpModule.Init(HttpApplication context)
{
context.PreSendRequestHeaders += (sender, e) =>
{
var response = context.Response;
if (context.Request.Headers[ORIGINHEADER] == ALLOWEDORIGIN)
{
response.Headers.Add("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
response.Headers.Add("Access-Control-Allow-Headers", "Content-Type");
}
if (context.Request.HttpMethod.ToUpperInvariant() == OPTIONSMETHOD && context.Request.Headers[ORIGINHEADER] == ALLOWEDORIGIN)
{
response.Headers.Clear();
response.Headers.Add("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
response.Headers.Add("Access-Control-Allow-Origin", "https://yourspodomain.sharepoint.com");
response.Headers.Add("Access-Control-Allow-Credentials", "true");
response.Headers.Add("Access-Control-Allow-Headers", "Content-Type");
response.Clear();
response.StatusCode = (int)HttpStatusCode.OK;
}
};
}
}
My self hosted web api is like this:
Program.cs
static void Main()
{
try
{
ServiceBase[] ServicesToRun;
ServicesToRun = new ServiceBase[]
{
new APIServiceTest()
};
ServiceBase.Run(ServicesToRun);
}
catch (Exception ex)
{
throw ex;
}
}
class Startup
{
// Hack from https://stackoverflow.com/a/17227764/19020 to load controllers in
// another assembly. Another way to do this is to create a custom assembly resolver
//Type valuesControllerType = typeof(OWINTest.API.ValuesController);
// This code configures Web API. The Startup class is specified as a type
// parameter in the WebApp.Start method.
public void Configuration(IAppBuilder appBuilder)
{
try
{
//Debugger.Launch();
// Configure Web API for self-host.
HttpConfiguration config = new HttpConfiguration();
config.MessageHandlers.Add(new CustomHeaderHandler());
var corsAttr = new EnableCorsAttribute(System.Configuration.ConfigurationManager.AppSettings["DominioSharePoint"].ToString(), "*", "*");
config.EnableCors(corsAttr);
// Enable attribute based routing
// http://www.asp.net/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
appBuilder.UseWebApi(config);
}
catch (Exception ex)
{
throw ex;
}
}
}
My controller:
[EnableCors(origins: "https://xx.sharepoint.com", headers: "*", methods: "*")]
public class CuentasCobroController : ApiController
{
However because its self hosted I cant implement an IHttpModule there as explained above, but I can create a custom handler how can I implemente the code above from the blog in the custom handler?
public class CustomHeaderHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
{
return base.SendAsync(request, cancellationToken)
.ContinueWith((task) =>
{
HttpResponseMessage response = task.Result;
response.Headers.Add("Access-Control-Allow-Origin", "*");
return response;
});
}
}
Question is, how can I integrate first code, into the startup of my windows service?
You are almost there by using DelegatingHandler instead of IHttpModule.
config.MessageHandlers.Add(new CorsHeaderHandler());
DelegatingHandler.SendAsync can access both request and response.
public class CorsHeaderHandler : DelegatingHandler
{
private const string OPTIONSMETHOD = "OPTIONS";
private const string ORIGINHEADER = "ORIGIN";
private const string ALLOWEDORIGIN = "https://yourspodomain.sharepoint.com";
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
{
return base.SendAsync(request, cancellationToken).ContinueWith(task =>
{
var allowedOrigin = request.Headers.Any(t => t.Key == ORIGINHEADER && t.Value.Contains(ALLOWEDORIGIN));
if (allowedOrigin == false) return task.Result;
if (request.Method == HttpMethod.Options)
{
var emptyResponse = new HttpResponseMessage(HttpStatusCode.OK);
emptyResponse.Headers.Add("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
emptyResponse.Headers.Add("Access-Control-Allow-Origin", ALLOWEDORIGIN);
emptyResponse.Headers.Add("Access-Control-Allow-Credentials", "true");
emptyResponse.Headers.Add("Access-Control-Allow-Headers", "Content-Type");
return emptyResponse;
}
else
{
task.Result.Headers.Add("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
task.Result.Headers.Add("Access-Control-Allow-Headers", "Content-Type");
return task.Result;
}
});
}
}
Simply put, you can't use IHttpModule with self-hosted Web API, or anything that is not IIS. IHttpModule is an IIS concept only.
What you can do instead, as you have discovered, is you can modify the Web API pipeline and insert your code (or the Web API equivalent) there instead. This could be done with a DelegatingHandler, or an action filter.
The simplest solution, however, would be to simply use the Microsoft.AspNet.WebApi.Cors NuGet package. With an HttpConfiguration object, call .EnableCors(...) and pass in an EnableCorsAttribute object as per the instructions here from Microsoft.
This is what you've already done in your code above, but you seem to be trying to also add in the custom CORS code from your HTTP module. If you remove the EnableCors attribute from your controller, and remove your CustomHeaderHandler, it should work as you would expect.

How to prioritize Web Api Controllers over IHttpHandler?

I have a legacy project that has a single IHttpHandler implementing class that routes all the requests using a huge switch statement etc.. I am trying to introduce Attribute Routing with ApiControllers but the first one always has the priority. Is it possible to configure the system (either code or IIS) so that Web ApiControllers have priority over my single IHttpHandler implementing class? In IIS, I put my AttributeRouting first and then there are all the aspx ones but still the Web Api Controller is not getting processed first..no matter what I do (having them under the same project). I don't want to introduce a separate project.
Edit: There is a IHttpModule that decides based on what is after api/ to route it to specific ashx file. One of them is the one described..
Edit 2: More specifically: If the uri doesn't have a list of filtered things [file,message,property ...] it is routed to Resource.aspx
so api/file, api/message, api/property would be handle from other .ashx files - otherwise the traffic goes to Resource.ashx...
As a result the requests that have api/endpoint1, api/endpoint2, api/endpoint3
will all go to Resource.aspx. The question is how to route api/endpoint3 to the API Controller described below.
Thanks
Simplified Code Architecture:
//SolutionName/Api/MyModule.cs (Legacy Code)
//this routes based on what is after api/ to Resource.ashx or other ashx files
public class MyModule : IHttpModule {
//if url doesn't contain [file,message,property ...] route to Resource.ashx
}
//SolutionName/API/Resource.ashx (Legacy Code)
//this is hit at any request solutionname/api/anything
public class DefaultHandler : IHttpHandler
{
public void ProcessRequest(HttpContext context) {
String APIBranch = parse(context);
switch(APIBranch)
{
case "endpoint1": methodOne(); break;
case "endpoint2": methodTwo(); break;
[...]
default: throw Exception(); break;
}
}
}
//SolutionName/API/App_Start/AttributeRoutingHttpConfig.cs
public static class AttributeRoutingHttpConfig
{
public static void RegisterRoutes(HttpRouteCollection routes)
{
// See http://github.com/mccalltd/AttributeRouting/wiki for more options.
// To debug routes locally using the built in ASP.NET development server, go to /routes.axd
routes.MapHttpAttributeRoutes();
}
public static void Start()
{
RegisterRoutes(GlobalConfiguration.Configuration.Routes);
}
}
//SolutionName/API/Controllers/MyController.cs
//this should have been hit for a GET on solutionname/api/endpoint3/id
[RoutePrefix("endpoint3")]
public class MyController : ApiController
{
private IModelDao modelDao;
MyController(IModelDao modelDao){
this.modelDao = modelDao;
}
[Route("{id}")]
[HttpGet]
public Model GetSomething(int id)
{
Model model = modelDao.GetSomething(id);
return model;
}
}
I've found two solutions to this problem. The first is to modify module that rewrites urls by inserting check if Web API routing system can handle request. The second is to add another module to application, that will direct requests to Web API Handler using HttpContext.RemapHandler().
Here's code:
First solution.
If your module looks like this:
public class MyModule: IHttpModule
{
public void Dispose(){}
public void Init(HttpApplication context)
{
context.BeginRequest += (object Sender, EventArgs e) =>
{
HttpContext httpContext = HttpContext.Current;
string currentUrl = httpContext.Request.Url.LocalPath.ToLower();
if (currentUrl.StartsWith("/api/endpoint0") ||
currentUrl.StartsWith("/api/endpoint1") ||
currentUrl.StartsWith("/api/endpoint2"))
{
httpContext.RewritePath("/api/resource.ashx");
}
};
}
}
Then you need to change it like this:
public void Init(HttpApplication context)
{
context.BeginRequest += (object Sender, EventArgs e) =>
{
HttpContext httpContext = HttpContext.Current;
var httpRequestMessage = new HttpRequestMessage(
new HttpMethod(httpContext.Request.HttpMethod),
httpContext.Request.Url);
IHttpRouteData httpRouteData =
GlobalConfiguration.Configuration.Routes.GetRouteData(httpRequestMessage);
if (httpRouteData != null) //enough if WebApiConfig.Register is empty
return;
string currentUrl = httpContext.Request.Url.LocalPath.ToLower();
if (currentUrl.StartsWith("/api/endpoint0") ||
currentUrl.StartsWith("/api/endpoint1") ||
currentUrl.StartsWith("/api/endpoint2"))
{
httpContext.RewritePath("/api/resource.ashx");
}
};
}
Second solution.
Module for remapping handlers:
public class RemappingModule: IHttpModule
{
public void Dispose() { }
public void Init(HttpApplication context)
{
context.PostResolveRequestCache += (src, args) =>
{
HttpContext httpContext = HttpContext.Current;
string currentUrl = httpContext.Request.FilePath;
if (!string.IsNullOrEmpty(httpContext.Request.QueryString.ToString()))
currentUrl += "?" + httpContext.Request.QueryString;
//checking if url was rewritten
if (httpContext.Request.RawUrl != currentUrl)
{
//getting original url
string url = string.Format("{0}://{1}{2}",
httpContext.Request.Url.Scheme,
httpContext.Request.Url.Authority,
httpContext.Request.RawUrl);
var httpRequestMessage = new HttpRequestMessage(
new HttpMethod(httpContext.Request.HttpMethod), url);
//checking if Web API routing system can find route for specified url
IHttpRouteData httpRouteData =
GlobalConfiguration.Configuration.Routes.GetRouteData(httpRequestMessage);
if (httpRouteData != null)
{
//to be honest, I found out by experiments, that
//context route data should be filled that way
var routeData = httpContext.Request.RequestContext.RouteData;
foreach (var value in httpRouteData.Values)
routeData.Values.Add(value.Key, value.Value);
//rewriting back url
httpContext.RewritePath(httpContext.Request.RawUrl);
//remapping to Web API handler
httpContext.RemapHandler(
new HttpControllerHandler(httpContext.Request.RequestContext.RouteData));
}
}
};
}
}
These solutions work when method WebApiConfig.Register is empty, but if there were routes with templates like "api/{controller}" then any path with two segments starting with "api" would pass the check, even if there're no controllers with specified name and your module can do something userfull for this path. In this case you can, for example, use method from this answer to check if controller exists.
Also Web API routing system will accept route even if found controller don't handle requests for current http method. You can use descendant of RouteFactoryAttribute and HttpMethodConstraint to avoid this.
UPD Tested on this controllers:
[RoutePrefix("api/endpoint1")]
public class DefaultController : ApiController
{
[Route("{value:int}")]
public string Get(int value)
{
return "TestController.Get: value=" + value;
}
}
[RoutePrefix("api/endpoint2")]
public class Endpoint2Controller : ApiController
{
[Route("segment/segment")]
public string Post()
{
return "Endpoint2:Post";
}
}

Categories