WebApi 2 AssemblyResolver - c#

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();
}
}

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" ;
}

ASP.NET Self hosted Web API with IE – This page cannot be displayed

I am trying to invoke my service locally but IE and Edge are not able to find it.
Below is the code snippet I have and my console app is working without any error.
Program.cs
public class Program
{
static void Main()
{
string baseAddress = "http://127.0.0.1:8080/";
// Start OWIN host
using (WebApp.Start(url: baseAddress))
{
Console.WriteLine("Service Listening at " + baseAddress);
System.Threading.Thread.Sleep(-1);
}
}
}
Startup.cs
public class Startup
{
public void Configuration(IAppBuilder appBuilder)
{
HttpConfiguration config = new HttpConfiguration();
config.EnableCors();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
appBuilder.UseWebApi(config);
}
}
WebController.cs
public class Web
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
[EnableCors(origins: "*", headers: "*", methods: "*")]
public class WebController : ApiController
{
Web[] websites = new Web[]
{
new Web { Id = 1, Name = "XYZ", Description = "XYZ"},
new Web { Id = 2, Name = "ABC", Description = "ABC"}
};
// GET api/Web
public IEnumerable Get()
{
return websites;
}
// GET api/Web/5
public Web Get(int id)
{
try
{
return websites[id];
}
catch (Exception e)
{
return new Web();
}
}
// POST api/values
public void Post([FromBody]string value)
{
Console.WriteLine("Post method called with value = " + value);
}
// PUT api/values/5
public void Put(int id, [FromBody]string value)
{
Console.WriteLine("Put method called with value = " + value);
}
// DELETE api/values/5
public void Delete(int id)
{
Console.WriteLine("Delete method called with id = " + id);
}
}
I am invoking my service on IE like everyone does: http://127.0.0.1:8080/api/web to GET entire Web Object.
I have installed two additional packages, OWIN and CORS.
Could someone help find a solution for this issue.
I have just tried and your api works correctly in Edge and Chrome. It can be that IE don't send correct Accept header which causes to either server to return wrong result or error or IE cannot interpret the response. Actually, I have check and IE offers to save json file because it cannot display it correctly.
To show it explicitly I have modified you code a bit:
public IHttpActionResult Get(string type = null)
{
var response = new HttpResponseMessage(HttpStatusCode.OK);
if (type == "json")
{
response.Content = new StringContent(JsonConvert.SerializeObject(websites));
response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
}
else if (type == "xml")
{
response.Content = new StringContent("<xmlTag>Value</xmlTag>");
response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/xml");
}
return ResponseMessage(response);
}
Try follow http://127.0.0.1:8080/api/web?type=json and http://127.0.0.1:8080/api/web?type=xml urls and check yourself.

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.

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

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;
}
}

Categories