I have a bunch of custom routes defined using AttributeRouting. I have a function in a controller that is trying to access one of these API functions at /api/GetBatchItems.
GetBatchItems is a function of the controller APIController, similar to:
[RouteArea("api")]
public sealed class APIController : ApiController{
[GET("GetBatches")]
public IEnumerable<PRAT.Models.EF.EFBatchItem> GetBatches() { ... }
}
In another controller, I am trying to get the result. When browsing directly everything is fine if I do it this way, but I want to be able to use my already defined route, is there a way to do that? I saw someone mention HttpAttributeRoutingConfiguration but I could not find that class anywhere. I don't want to have to use the MapHttpRoute method this way...
var config = new HttpConfiguration();
config.Routes.MapHttpRoute("default", "api/{controller}/{id}", null);
var server = new HttpServer(config);
var client = new HttpClient(server);
string url = Request.Url.GetLeftPart(UriPartial.Authority) + "/api/APIController/GetBatches";
var result = client.GetAsync(url).Result;
var content = result.Content;
var model = content.ReadAsAsync<IEnumerable<PRAT.Models.EF.EFBatchItem>>().Result;
if (model == null) return View();
else return View(model);
TO MAKE YOUR SAMPLE CODE WORK
Your existing code sample needs two changes to work:
make {id} optional, since GetBatches() has no parameter:
config.Routes.MapHttpRoute("default", "api/{controller}/{id}", new { id = RouteParameter.Optional });
since HttpConfiguration routes match {controller} to a class named {controller}Controller, change your url calculation to:
string url = Request.Url.GetLeftPart(UriPartial.Authority) + "/api/API/GetBatches";
SIMPLER VERSION
Your can use your existing route /api/GetBatches
var client = new HttpClient();
string url = Request.Url.GetLeftPart(UriPartial.Authority) + "/api/GetBatches";
var result = client.GetAsync(url).Result;
var content = result.Content;
var model = content.ReadAsAsync<IEnumerable<MyViewModel>>().Result;
if (model == null) return View();
else return View(model);
EVEN SIMPLER (IF YOU DO NOT NEED HTTP)
Replace your sample code with this Add this extension class:
var model = (new APIController()).GetBatches();
if (model == null) return View();
else return View(model);
Related
I wanted to make my WebAPI application change the used SessionStateBehavior based on action attributes like that:
[HttpPost]
[Route("api/test")]
[SetSessionStateBehavior(SessionStateBehavior.Required)] // <--- This modifies the behavior
public async Task<int> Test(){}
It seems, however, that the only place I can change the session behavior is inside my HttpApplication's Application_PostAuthorizeRequest (or in similar places, early in the request lifetime), otherwise I get this error:
'HttpContext.SetSessionStateBehavior' can only be invoked before 'HttpApplication.AcquireRequestState' event is raised.
So, at that point no controller or action resolution is done, so I don't know what action will be called in order to check its attributes.
So, I am thinking of resolving the action manually.
I started with these lines of code to resolve the controller first:
var httpCtx = HttpContext.Current;
var ctrlSel = GlobalConfiguration.Configuration.DependencyResolver.GetService(typeof(IHttpControllerSelector)) as IHttpControllerSelector;
var actionSel = GlobalConfiguration.Configuration.DependencyResolver.GetService(typeof(IHttpActionSelector)) as IHttpActionSelector;
HttpControllerDescriptor controllerDescriptor = ctrlSel.SelectController(httpCtx.Request);
But in the last line I can't get the proper HttpRequestMessage from the request.
Any idea ho how get that?
This is not inside a controller, so I don't have it ready there.
Or, is there a better way to do this?
I am trying to see the disassembled code of the framework to copy portions of it, but I am quite lost at this point...
UPDATE:
This is the closest I got to resolving the action manually, but it doesn't work:
I have registered those two services:
container.RegisterType<IHttpControllerSelector, DefaultHttpControllerSelector>();
container.RegisterType<IHttpActionSelector, ApiControllerActionSelector>();
...and try to get the required session behavior like that:
private SessionStateBehavior GetDesiredSessionBehavior(HttpContext httpCtx)
{
var config = GlobalConfiguration.Configuration;
var diResolver = config.Services;
var ctrlSel = diResolver.GetService(typeof(IHttpControllerSelector)) as IHttpControllerSelector;
var actionSel = diResolver.GetService(typeof(IHttpActionSelector)) as IHttpActionSelector;
if (ctrlSel is null || actionSel is null)
{
return DefaultSessionBehavior;
}
var method = new HttpMethod(httpCtx.Request.HttpMethod);
var requestMsg = new HttpRequestMessage(method, httpCtx.Request.Url);
requestMsg.Properties.Add(HttpPropertyKeys.RequestContextKey, httpCtx.Request.RequestContext);
requestMsg.Properties.Add(HttpPropertyKeys.HttpConfigurationKey, config);
httpCtx.Request.Headers.Cast<string>().ForEach(x => requestMsg.Headers.Add(x, httpCtx.Request.Headers[x]));
var httpRouteData = httpCtx.Request.RequestContext.RouteData;
var routeData = config.Routes.GetRouteData(requestMsg);
requestMsg.Properties.Add(HttpPropertyKeys.HttpRouteDataKey, routeData);
requestMsg.SetRequestContext(new HttpRequestContext(){RouteData = routeData });
requestMsg.SetConfiguration(config);
var route = config.Routes["DefaultApi"];
requestMsg.SetRouteData(routeData ?? route.GetRouteData(config.VirtualPathRoot, requestMsg));
var routeHandler = httpRouteData.RouteHandler ?? new WebApiConfig.SessionStateRouteHandler();
var httpHandler = routeHandler.GetHttpHandler(httpCtx.Request.RequestContext);
if (httpHandler is IHttpAsyncHandler httpAsyncHandler)
{
httpAsyncHandler.BeginProcessRequest(httpCtx, ar => httpAsyncHandler.EndProcessRequest(ar), null);
}
else
{
httpHandler.ProcessRequest(httpCtx);
}
var values = requestMsg.GetRouteData().Values; // Hm this is empty and makes the next call fail...
HttpControllerDescriptor controllerDescriptor = ctrlSel.SelectController(requestMsg);
IHttpController controller = controllerDescriptor?.CreateController(requestMsg);
if (controller == null)
{
return DefaultSessionBehavior;
}
var ctrlContext = CreateControllerContext(requestMsg, controllerDescriptor, controller);
var actionCtx = actionSel.SelectAction(ctrlContext);
var attr = actionCtx.GetCustomAttributes<ActionSessionStateAttribute>().FirstOrDefault();
return attr?.Behavior ?? DefaultSessionBehavior;
}
I have an alternative hack to make it work (send header values from the client to modify the session behavior), but it would be nice if the version above worked.
UPDATE:
Eventually, I went with setting the session behavior based on a client header value and validating the validity of sending that header based on the action attributes later-on in the request lifetime. If someone can solve the action resolution code I was fighting with above, feel free to post the answer here.
I don't know if this is going to be helpful for you, but I was just following a Pluralsight course (https://app.pluralsight.com/player?course=implementing-restful-aspdotnet-web-api) and in the Versioning chapter the author shows how to implement a controller selector where he does have access to the request.
The controller selector looks like:
public class CountingKsControllerSelector : DefaultHttpControllerSelector
{
private HttpConfiguration _config;
public CountingKsControllerSelector(HttpConfiguration config)
: base(config)
{
_config = config;
}
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
var controllers = GetControllerMapping();
var routeData = request.GetRouteData();
var controllerName = (string)routeData.Values["controller"];
HttpControllerDescriptor descriptor;
if (controllers.TryGetValue(controllerName, out descriptor))
{
[...]
return descriptor;
}
return null;
}
}
And it's registered in WebApiConfig with:
config.Services.Replace(typeof(IHttpControllerSelector),
new CountingKsControllerSelector(config));
I have a controller which performs certain action. Now, i need certain functionality (send an email) to happen in middle of the above mentioned controller.
try
{
using (HttpClient client = new HttpClient())
{
// Add an Accept header for JSON format
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
response = client.PostAsJsonAsync(URLForReportsapi + "InsertOrUpdatePortal", criteria).Result;
}
// Throw exception if not a success code.
response.EnsureSuccessStatusCode();
// Parse the response body.
var pVMRslt = response.Content.ReadAsStringAsync().Result;
var pVMRsltDsrlz = JsonConvert.DeserializeObject<List<PortalInfoVM>>(pVMRslt);
portalVMList = pVMRsltDsrlz.ToList();
var newnumids = portalVMList.Where(c => c.isNewStatus == "NEW").Select(c => c. numid).ToList();
//-------- Here I need to call another action which is residing inside another controller.
return PartialView("AddToPortalConfirmation", portalVMList);
}
In the above piece of code before calling the partialview I need to call another action which is residing inside a different controller and return back to PartialView("AddToPortalConfirmation", portalVMList)
I was trying to do something like this but this is not working.
var resp = RedirectToAction("Create", "EmailReport", new { numids = newnumids , To = toEmail});
In my routeconfig.cs I have
routes.MapRoute(
null, // Route name
"EmailReport/Create/{To}/{numids }", // URL with parameters
new { controller = "EmailReport", action = "Create", To = string.Empty, numids = string.Empty } // Parameter defaults
);
Try :
return RedirectToAction("Create", "EmailReport", new { numids = newnumids , To = toEmail});
I am trying to unit test my controller, but as soon as this controller uses its embedded UrlHelper object, it throws an ArgumentNullException.
The action I'm trying to test is this one:
public HttpResponseMessage PostCommandes(Commandes commandes)
{
if (this.ModelState.IsValid)
{
this.db.AddCommande(commandes);
HttpResponseMessage response = this.Request.CreateResponse(HttpStatusCode.Created, commandes);
// this returns null from the test project
string link = this.Url.Link(
"DefaultApi",
new
{
id = commandes.Commande_id
});
var uri = new Uri(link);
response.Headers.Location = uri;
return response;
}
else
{
return this.Request.CreateResponse(HttpStatusCode.BadRequest);
}
}
My test method looks like this:
[Fact]
public void Controller_insert_stores_new_item()
{
// arrange
bool isInserted = false;
Commandes item = new Commandes() { Commande_id = 123 };
this.fakeContainer.AddCommande = (c) =>
{
isInserted = true;
};
TestsBoostrappers.SetupControllerForTests(this.controller, ControllerName, HttpMethod.Post);
// act
HttpResponseMessage result = this.controller.PostCommandes(item);
// assert
result.IsSuccessStatusCode.Should().BeTrue("because the storage method should return a successful HTTP code");
isInserted.Should().BeTrue("because the controller should have called the underlying storage engine");
// cleanup
this.fakeContainer.AddCommande = null;
}
And the SetupControllerForTests method is this one, as seen here:
public static void SetupControllerForTests(ApiController controller, string controllerName, HttpMethod method)
{
var request = new HttpRequestMessage(method, string.Format("http://localhost/api/v1/{0}", controllerName));
var config = new HttpConfiguration();
var route = WebApiConfig.Register(config).First();
var routeData = new HttpRouteData(route, new HttpRouteValueDictionary
{
{
"controller",
controllerName
}
});
controller.ControllerContext = new HttpControllerContext(config, routeData, request);
controller.Request = request;
controller.Request.Properties[HttpPropertyKeys.HttpConfigurationKey] = config;
controller.Request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData;
}
This is a pretty well documented problem for WebApi2, you can read more about it here for instance ("testing link generation"). Basically, it boils down to either setting a custom ApiController.RequestContext, or mocking the controller's Url property.
The problem is that, in my version of WebApi (Nuget packages: Microsoft.AspNet.WebApi 4.0.20710.0 / WebApi.Core.4.0.30506.0), ApiController.RequestContext does not exist, and Moq cannot mock the UrlHelper class, because the method it should mock (Link) is not overridable, or something like that (I didn't dwell on it). Because I'm using WebApi 1. But the blog post I based my code on (as well as many other posts) use V1, too. So I don't understand why it doesn't work, and most of all, how I can make it work.
Thank you !
Not sure if the documentation you linked to was updated since your original post but they show an example where they mock up the UrlHelper and also the Link method.
[TestMethod]
public void PostSetsLocationHeader_MockVersion()
{
// This version uses a mock UrlHelper.
// Arrange
ProductsController controller = new ProductsController(repository);
controller.Request = new HttpRequestMessage();
controller.Configuration = new HttpConfiguration();
string locationUrl = "http://location/";
// Create the mock and set up the Link method, which is used to create the Location header.
// The mock version returns a fixed string.
var mockUrlHelper = new Mock<UrlHelper>();
mockUrlHelper.Setup(x => x.Link(It.IsAny<string>(), It.IsAny<object>())).Returns(locationUrl);
controller.Url = mockUrlHelper.Object;
// Act
Product product = new Product() { Id = 42 };
var response = controller.Post(product);
// Assert
Assert.AreEqual(locationUrl, response.Headers.Location.AbsoluteUri);
}
So, you need to mock UrlHelper.Link method. It can be easily done with Typemock Isolator (test example from given link):
[TestMethod, Isolated]
public void PostSetsLocationHeader_MockVersion()
{
// This version uses a mock UrlHelper.
// Arrange
ProductsController controller = new ProductsController(repository);
controller.Request = new HttpRequestMessage();
controller.Configuration = new HttpConfiguration();
string locationUrl = "http://location/";
// Create the mock and set up the Link method, which is used to create the Location header.
// The mock version returns a fixed string.
var mockUrlHelper = Isolate.Fake.Instance<UrlHelper>();
Isolate.WhenCalled(() => mockUrlHelper.Link("", null)).WillReturn(locationUrl);
controller.Url = mockUrlHelper;
// Act
Product product = new Product() { Id = 42 };
var response = controller.Post(product);
// Assert
Assert.AreEqual(locationUrl, response.Headers.Location.AbsoluteUri);
}
In an existing C# Web project here at my Job I've added a Web API part.
In four of my own classes that I use for the Web API I need to access some of the existing Controller-classes. Right now I just create a new Instance of them and everything works as intented: ProductController controller = new ProductController();
Still, creating a new ProductController while one should already exist obviously isn't a good practice. I know the Controllers are created in the Config-file in the Routes.MapHttpRoute, since it's using the C# Web MVC method. Below I've copied that piece of code:
config.Routes.MapHttpRoute(
name: "Default",
routeTemplate: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new[] { "MyProject.Controllers" }
);
route.DataTokens["UseNamespaceFallback"] = false;
I've tried to access these Controllers in my one of my API-classes like so:
private void getControllerInstance()
{
var url = "~/Products";
// Original path is stored and will be rewritten in the end
var httpContext = new HttpContextWrapper(HttpContext.Current);
string originalPath = httpContext.Request.Path;
try
{
// Fake a request to the supplied URL into the routing system
httpContext.RewritePath(url);
RouteData urlRouteData = RouteTable.Routes.GetRouteData(httpContext);
// If the route data was not found (e.g url leads to another site) then authorization is denied.
// If you want to have a navigation to a different site, don't use AuthorizationMenu
if (urlRouteData != null)
{
string controllerName = urlRouteData.Values["controller"].ToString();
// Get an instance of the controller that would handle this route
var requestContext = new RequestContext(httpContext, urlRouteData);
var controllerFactory = ControllerBuilder.Current.GetControllerFactory();
// TODO: Fix error (The controller for path '/Products' was not found or does not implement IController.) on this line:
var controllerbase = (ControllerBase)controllerFactory.CreateController(requestContext, controllerName);
controller = (ProductController)controllerbase;
}
}
finally
{
// Reset our request path.
httpContext.RewritePath(originalPath);
}
}
As you might have noticed by the TODO-comment, at the line var controllerbase = (ControllerBase)controllerFactory.CreateController(requestContext, controllerName);, I get the following error:
HttpException was unhandler by user code: The controller for path '/Products' was not found or does not implement IController.
Does anyone know how to fix this error? Has this got something to do with one of the following two lines of the code in the Config-file?
namespaces: new[] { "MyProject.Controllers" }
route.DataTokens["UseNamespaceFallback"] = false;
Or did I do something else wrong?
A tip to everyone: Don't continue programming when you are very, very tired.. Anyway, everything was correct except for a small flaw:
My API Controller is called ProductsController and my normal (default) controller is called ProductController. In the method above I use:
var url = "~/Products";
To access the ProductController..
So, after removing the "s" (and for good measure make everything lower case) I have the following instead:
var url = "~/product";
And now it works..
I've the following ASP.NET WebAPI binding:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new {id = RouteParameter.Optional}
);
And my Controller looks like this:
public class ReferenceDataController : BaseController
{
[RequireUserToken(ApprovedDeviceToken = true, ValidUserToken = true)]
[HttpPost]
public IEnumerable<SynchronizeItem<IReferenceDataItem>> Sync([FromBody]IEnumerable<SynchronizeItem<IReferenceDataItem>> clientSyncItems, [FromUri]int referenceDataType)
{
// my code
}
On the client site I use the following code to send a request:
var client = new RestClient (baseUrl);
var request = new RestRequest (resource, method);
request.XmlSerializer = new JsonSerializer ();
request.RequestFormat = DataFormat.Json;
request.AddHeader ("X-Abc-DeviceToken", deviceToken);
if (!string.IsNullOrWhiteSpace (userToken))
request.AddHeader ("X-Abc-UserToken", userToken);
if (payload != null)
request.AddBody (payload);
if (parameters != null)
{
foreach (var parameter in parameters)
{
request.AddUrlSegment(parameter.Key, parameter.Value);
}
}
var response = client.Execute<T> (request);
My expectation is, sending a POST request to http://myhost/api/referencedata/sync?referencedatatype=countries with a body which contains an IEnumerable. If I remove the UrlSegment parameters on client site and the second argument on the webservice site, than it works.
How can I combine a body with payload and additional URL parameters?
You can define your action method as follow,
[RequireUserToken(ApprovedDeviceToken = true, ValidUserToken = true)]
[HttpPost]
public IEnumerable<SynchronizeItem<IReferenceDataItem>> Sync(IEnumerable<SynchronizeItem<IReferenceDataItem>> clientSyncItems, int referenceDataType)
{
// my code
}
No BodyAttribute or FromUriAttribute. In that way, Web API will try to use a MediaTypeFormatter to deserialize the body into the clientSyncItems collection and any additional value type from the query string (referenceDataType from the query string). The route as you defined it will take "sync" as Id (which would be ignored as it is not a parameter in your action).
You must also specify a content-type header so Web API can choose the right formatter (json or xml for example).