Get matched route name in Web API - asp.net

In my Web API handler I need to get the name of the route that matched the request.
public class CurrentRequestMessageHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var route = request.GetRouteData().Route;
//now what?
return base.SendAsync(request, cancellationToken);
}
}

Currently there is no way to retrieve the route name of a route in Web API. You can take a look at the HttpRouteCollection source code here for more details. If route name is definitely required for your scenario, you could stick in the route name in the data tokens of a route. (note that currently attribute routing doesn't provide a way to access the data tokens)
Update - 6/23/2014
With latest improvements(5.2 RC) in the area of attribute routing, you can do something like following to insert route names into data tokens.
config.MapHttpAttributeRoutes(new CustomDefaultDirectRouteProvider());
public class CustomDefaultDirectRouteProvider : DefaultDirectRouteProvider
{
public override IReadOnlyList<RouteEntry> GetDirectRoutes(HttpControllerDescriptor controllerDescriptor,
IReadOnlyList<HttpActionDescriptor> actionDescriptors, IInlineConstraintResolver constraintResolver)
{
IReadOnlyList<RouteEntry> coll = base.GetDirectRoutes(controllerDescriptor, actionDescriptors, constraintResolver);
foreach(RouteEntry routeEntry in coll)
{
if (!string.IsNullOrEmpty(routeEntry.Name))
{
routeEntry.Route.DataTokens["Route_Name"] = routeEntry.Name;
}
}
return coll;
}
}
Access it like this:
reequest.GetRouteData().Route.DataTokens["Route_Name"]

It's perhaps a bit late to answer this, but I found myself in the same situation (that is I need to generate an URL while not having the corresponding IHttpRoute name). You can however generate an URL with just the Route and the HttpRequestMessage.
var parameters = new Dictionary{{"id" , 123}, {HttpRoute.HttpRouteKey, true}};
var path = Route.GetVirtualPath(request, parameters);
var uri = path.VirtualPath;
The important part is to add HttpRoute.HttpRouteKey to the parameters, if this value is not used GetVirtualPath returns null.
see code in HttpRoute.cs
// Only perform URL generation if the "httproute" key was specified. This allows these
// routes to be ignored when a regular MVC app tries to generate URLs. Without this special
// key an HTTP route used for Web API would normally take over almost all the routes in a
// typical app.
if (values != null && !values.Keys.Contains(HttpRouteKey, StringComparer.OrdinalIgnoreCase))
{
return null;
}

Related

Changing Request Path in .Net Core 3.1

Prior to 3.0, I could change the path of a request (without any form of browser redirection) by just accessing the HttpRequest property of the HttpContext and then changed the value of the Path.
As an example, to display a page for a user who needed to change his/her password (irrespective of the page the user intended to visit), I extended the HttpContext
public static void ChangeDefaultPassword(this HttpContext context)
=> context.Request.Path = "/Account/ChangePassword";
This piece of code takes the user to the action method ChangePassword in the AccountController without executing the action method the user intends to visit.
Then enters dotnet core 3.1.
In 3.1, the extension method changes the path. However, it never executes the action method. It ignores the updated path.
I am aware this is due to the changes in the routing.The endpoint can now be accessed with the extension method HttpContext.GetEndpoint(). There is also an extension method HttpContext.SetEndpoint which seems to be the right way to set a new endpoint. However, there is no sample of how to accomplish this.
The Question
How do I change the request path, without executing the original path?
What I Have Tried
I tried changing the path. It seems routing in dotnet core 3.1 ignores the value of the HttpRequest path value.
I tried redirecting with context.Response.Redirect("/Account/ChangePassword");. This worked but it first executed the original action method requested by the user. This behavior defeated the purpose.
I tried using the extension method HttpContext.SetEndpoint, but there was no example available to work with.
The way I worked around this issue is to use EndpointDataSource directly, which is a singleton service that is available from DI as long as you have the routing services registered. It works as long as you can provide the controller name and the action name, which you can specify at compile time. This negates the need to use IActionDescriptorCollectionProvider or build the endpoint object or request delegate by yourself (which is pretty complicated...):
public static void RerouteToActionMethod(this HttpContext context, EndpointDataSource endpointDataSource, string controllerName, string actionName)
{
var endpoint = endpointDataSource.Endpoints.FirstOrDefault(e =>
{
var descriptor = e.Metadata.GetMetadata<ControllerActionDescriptor>();
// you can add more constraints if you wish, e.g. based on HTTP method, etc
return descriptor != null
&& actionName.Equals(descriptor.ActionName, StringComparison.OrdinalIgnoreCase)
&& controllerName.Equals(descriptor.ControllerName, StringComparison.OrdinalIgnoreCase);
});
if (endpoint == null)
{
throw new Exception("No valid endpoint found.");
}
context.SetEndpoint(endpoint);
}
I was able to find a working solution. My solution works by manually setting a new endpoint with the SetEndpoint extension method.
Here is an extension method I created to resolve this issue.
private static void RedirectToPath(this HttpContext context, string controllerName, string actionName )
{
// Get the old endpoint to extract the RequestDelegate
var currentEndpoint = context.GetEndpoint();
// Get access to the action descriptor collection
var actionDescriptorsProvider =
context.RequestServices.GetRequiredService<IActionDescriptorCollectionProvider>();
// Get the controller aqction with the action name and the controller name.
// You should be redirecting to a GET action method anyways. Anyone can provide a better way of achieving this.
var controllerActionDescriptor = actionDescriptorsProvider.ActionDescriptors.Items
.Where(s => s is ControllerActionDescriptor bb
&& bb.ActionName == actionName
&& bb.ControllerName == controllerName
&& (bb.ActionConstraints == null
|| (bb.ActionConstraints != null
&& bb.ActionConstraints.Any(x => x is HttpMethodActionConstraint cc
&& cc.HttpMethods.Contains(HttpMethods.Get)))))
.Select(s => s as ControllerActionDescriptor)
.FirstOrDefault();
if (controllerActionDescriptor is null) throw new Exception($"You were supposed to be redirected to {actionName} but the action descriptor could not be found.");
// Create a new route endpoint
// The route pattern is not needed but MUST be present.
var routeEndpoint = new RouteEndpoint(currentEndpoint.RequestDelegate, RoutePatternFactory.Parse(""), 1, new EndpointMetadataCollection(new object[] { controllerActionDescriptor }), controllerActionDescriptor.DisplayName);
// set the new endpoint. You are assured that the previous endpoint will never execute.
context.SetEndpoint(routeEndpoint);
}
Important
You must make the view of the action method available by placing it in the Shared folder. Alternatively, you may decide to provide a custom implementation of IViewLocationExpander
Before accessing the endpoint, the routing middleware must have executed.
USAGE
public static void ChangeDefaultPassword(this HttpContext context)
=> context.RedirectToPath("Account","ChangePassword");
Check your middleware order.
The middleware exposed by .UseRouting() is what's responsible for deciding which endpoint to hit based on the incoming request path. If your path rewrite middleware comes later in the pipeline (like mine was), it'll be too late and the routing decision has been made.
Moving my custom middleware before UseRouting() ensured that the path was set as I needed it before the routing middleware had been hit.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, TelemetryConfiguration telemetryConfig)
{
//snip
app.UseMiddleware<PathRewritingMiddleware>();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
//snip
}
I had a similar reroute issue. In my case, I want to reroute users to a "you don't have permissions" view when an AuthorationHandler fails. I applied the following code, notably (httpContext.Response.Redirect(...)) in (.Net Core 3.1) to route me to a NoPermissions action on a Home Controller.
In the handler class:
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, FooBarRequirement requirement) {
var hasAccess = await requirement.CheckAccess(context.User);
if (hasAccess)
context.Succeed(requirement);
else {
var message = "You do not have access to this Foobar function";
AuthorizeHandler.NoPermission(mHttpContextAccessor.HttpContext, context, requirement, message);
}
}
I wrote a static class to handle the redirect, passing in the url expected by the controller and action plus an error message, and the redirect permanent flag set to true:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
namespace Foo.BusinessLogic.Security {
public static class AuthorizeHandler {
public static void NoPermission(HttpContext httpContext,
AuthorizationHandlerContext context, IAuthorizationRequirement requirement, string
errorMessage) {
context.Succeed(requirement);
httpContext.Response.Redirect($"/home/nopermission/?m={errorMessage}", true);
}
}
}
Finally, the controller and action that handles the view and message
[AllowAnonymous]
public IActionResult NoPermission(string m) {
return View("NoPermission", m);
}
}
In my case, I am manually selecting the matching endpoint in a DynamicRouteValueTransformer. I have a mostly working solution but have to switch to other priorities. Perhaps someone else can create a more elegant solution using built in Action executors.
RequestDelegate requestDelegate = async (HttpContext x) =>
{//manually handle controller activation, method invocation, and result processing
var actionContext = new ActionContext(x, new RouteData(values), new ControllerActionDescriptor() { ControllerTypeInfo = controllerType.GetTypeInfo() });
var activator = x.RequestServices.GetService(typeof(IControllerActivator)) as ServiceBasedControllerActivator;
var controller = activator.Create(new ControllerContext(actionContext));
var arguments = methodInfo.GetParameters().Select(p =>
{
object r;
if (requestData.TryGetValue(p.Name, out object value)) r = value;
else if (p.ParameterType.IsValueType) r = Activator.CreateInstance(p.ParameterType);
else r = null;
return r;
});
var actionResultTask = methodInfo.Invoke(controller, arguments.ToArray());
var actionTask = actionResultTask as Task<IActionResult>;
if (actionTask != null)
{
var actionResult = await actionTask;
await actionResult.ExecuteResultAsync(actionContext);//errors here. actionContext is incomplete
}
};
var endpoint = new Endpoint(requestDelegate, EndpointMetadataCollection.Empty, methodInfo.Name);
httpContext.SetEndpoint(endpoint);

Adding security to RESTful API [closed]

Closed. This question does not meet Stack Overflow guidelines. It is not currently accepting answers.
We don’t allow questions seeking recommendations for books, tools, software libraries, and more. You can edit the question so it can be answered with facts and citations.
Closed 6 years ago.
Improve this question
I am wanting to implement two websites that need to communicate with each other. (Since one of the sites has a separate deployment for each customer, and is spread across many servers, sharing a database or communicating privately is not an option.) So I've been looking into RESTful APIs.
Unfortunately, I'm running into a lot of information that I'm not familiar with. One issue is security. We don't need anything fancy--we're not a bank or anything. I think we can just get away with HTTPS and a basic username and password.
Questions:
How would I pass the username and password to the API? Would they just be passed as bare arguments in the URL?
Does .NET provide any mechanism for authorizing such username and passwords, or do I just manually see if the password is in our database on each and every request? (I would hash for security.)
How would I pass the username and password to the API? Would they just
be passed as bare arguments in the URL?
It can be either in the URL or in the header. If you are using HTTPS, it will all be encrypted so it will not be bare. Please see this for more details.
Does .NET provide any mechanism for authorizing such username and
passwords, or do I just manually see if the password is in our
database on each and every request? (I would hash for security.)
No you do not need to check the database on every request. You can check once, create a token with an expiry and the client can keep sending you the token. This way you do not have to keep checking the database every single time.
Please see see this answer for some helpful information.
I think basic authentication with base64 encoding will be sufficient. If not you can always change it. Here are the different ways to apply it to your backend code:
To apply an authentication filter to a controller, decorate the controller class with the filter attribute. The following code sets the [IdentityBasicAuthentication] filter on a controller class, which enables Basic Authentication for all of the controller's actions.
[IdentityBasicAuthentication] // Enable Basic authentication for this controller.
[Authorize] // Require authenticated requests.
public class HomeController : ApiController
{
public IHttpActionResult Get() { . . . }
public IHttpActionResult Post() { . . . }
}
To apply the filter to one action, decorate the action with the filter. The following code sets the [IdentityBasicAuthentication] filter on the controller's Post method.
[Authorize] // Require authenticated requests.
public class HomeController : ApiController
{
public IHttpActionResult Get() { . . . }
[IdentityBasicAuthentication] // Enable Basic authentication for this action.
public IHttpActionResult Post() { . . . }
}
To apply the filter to all Web API controllers, add it to GlobalConfiguration.Filters.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Filters.Add(new IdentityBasicAuthenticationAttribute());
// Other configuration code not shown...
}
}
Finally here is an example of the implementation, you may change it as you need:
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Principal;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.Filters;
using BasicAuthentication.Results;
namespace BasicAuthentication.Filters
{
public abstract class BasicAuthenticationAttribute : Attribute, IAuthenticationFilter
{
public string Realm { get; set; }
public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
HttpRequestMessage request = context.Request;
AuthenticationHeaderValue authorization = request.Headers.Authorization;
if (authorization == null)
{
// No authentication was attempted (for this authentication method).
// Do not set either Principal (which would indicate success) or ErrorResult (indicating an error).
return;
}
if (authorization.Scheme != "Basic")
{
// No authentication was attempted (for this authentication method).
// Do not set either Principal (which would indicate success) or ErrorResult (indicating an error).
return;
}
if (String.IsNullOrEmpty(authorization.Parameter))
{
// Authentication was attempted but failed. Set ErrorResult to indicate an error.
context.ErrorResult = new AuthenticationFailureResult("Missing credentials", request);
return;
}
Tuple<string, string> userNameAndPasword = ExtractUserNameAndPassword(authorization.Parameter);
if (userNameAndPasword == null)
{
// Authentication was attempted but failed. Set ErrorResult to indicate an error.
context.ErrorResult = new AuthenticationFailureResult("Invalid credentials", request);
return;
}
string userName = userNameAndPasword.Item1;
string password = userNameAndPasword.Item2;
IPrincipal principal = await AuthenticateAsync(userName, password, cancellationToken);
if (principal == null)
{
// Authentication was attempted but failed. Set ErrorResult to indicate an error.
context.ErrorResult = new AuthenticationFailureResult("Invalid username or password", request);
}
else
{
// Authentication was attempted and succeeded. Set Principal to the authenticated user.
context.Principal = principal;
}
}
protected abstract Task<IPrincipal> AuthenticateAsync(string userName, string password,
CancellationToken cancellationToken);
private static Tuple<string, string> ExtractUserNameAndPassword(string authorizationParameter)
{
byte[] credentialBytes;
try
{
credentialBytes = Convert.FromBase64String(authorizationParameter);
}
catch (FormatException)
{
return null;
}
// The currently approved HTTP 1.1 specification says characters here are ISO-8859-1.
// However, the current draft updated specification for HTTP 1.1 indicates this encoding is infrequently
// used in practice and defines behavior only for ASCII.
Encoding encoding = Encoding.ASCII;
// Make a writable copy of the encoding to enable setting a decoder fallback.
encoding = (Encoding)encoding.Clone();
// Fail on invalid bytes rather than silently replacing and continuing.
encoding.DecoderFallback = DecoderFallback.ExceptionFallback;
string decodedCredentials;
try
{
decodedCredentials = encoding.GetString(credentialBytes);
}
catch (DecoderFallbackException)
{
return null;
}
if (String.IsNullOrEmpty(decodedCredentials))
{
return null;
}
int colonIndex = decodedCredentials.IndexOf(':');
if (colonIndex == -1)
{
return null;
}
string userName = decodedCredentials.Substring(0, colonIndex);
string password = decodedCredentials.Substring(colonIndex + 1);
return new Tuple<string, string>(userName, password);
}
public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
Challenge(context);
return Task.FromResult(0);
}
private void Challenge(HttpAuthenticationChallengeContext context)
{
string parameter;
if (String.IsNullOrEmpty(Realm))
{
parameter = null;
}
else
{
// A correct implementation should verify that Realm does not contain a quote character unless properly
// escaped (precededed by a backslash that is not itself escaped).
parameter = "realm=\"" + Realm + "\"";
}
context.ChallengeWith("Basic", parameter);
}
public virtual bool AllowMultiple
{
get { return false; }
}
}
}
If you still want to read more then here is a great article which goes into details. I have copied the above code from this article. It has lots of great information.
If you control or exert significant influence on both sides of the connection, client ssl certificates is a really strong and powerful way of doing this. It's attractive to me in this case because it only requires distributing a trusted CA certificate which can be done before the client certificates are created. It's far more secure than any username and password could ever be ( because the password doesn't need to go across the wire).
Any other solution with authentication I can think of, you're going to have to have some sort of data source to verify the credentials. But x509 solves this problem for you. We've done it at work between applications and other than managing the certificates it works really, really well. And it's basically the most secure thing available.
I don't know much about .net in general, but ( not to lmgtfy ) https://support.microsoft.com/en-us/kb/315588 seems like the step by step format you are looking for.
Just a thought, and it really depends on what you meant by "username/password". If this means "authorization"/access to some API call and you want to ensure that the client is "authorized" to make a call to your API (only apps A, B can make api calls to API - and it seems this is what you're looking for based on your comment above):
As in the comment above, authorization header, using JWT. There is an great/easy JWT library in Nuget
it's pretty much something like a "shared secret" used to sign a "payload" (the JWT)
the "sender" will build the JWT and sign it (and add to header or whatever protocol you want - it can be body if prefer it over headers)
the "receiver" will verify the JWT sent
this includes handling/mitigating "replays" - the JWT spec has an "expire" field (exp) that you can have the library validate as well (or not, it's up to you)
The project site is on Github with samples.
Hth.

Can this be done using ASP.NET URL Rewriting

There are about 1700 articles listed on my website created using ASP.NET 4.0 Web Forms. These articles have a url format as:
http://www.mymymyarticles.com/Article.aspx?ID=400
I have explored ASP.NET Friendly URLs as well IIS URL Rewrite. These extensions are great but once a rule is created, they treat all url's generically.
Is it possible that I manually generate my own url string for every url that exists on my website? For eg:
I want to permanent redirect http://www.mymymyarticles.com/Article.aspx?ID=400 to http://www.mymymyarticles.com/this-is-a-very-long-url whereas
http://www.mymymyarticles.com/Article.aspx?ID=500 can be redirected to http://www.mymymyarticles.com/article/short-url and
http://www.mymymyarticles.com/Article.aspx?ID=523 can be redirected to http://www.mymymyarticles.com/very-short-url
So you can see there is no uniformity in the url's that I want to manually generate. Basically I want full control over the url's. How can I go about this. Will it affect performance?
Any examples are appreciated.
Do you have a way of mapping an ID to the url of the new page? If that is the case, you could probably achieve this with ASP.NET Routing. What I would do is start with defining a route:
var route = routes.MapRoute(
"LegacyDocument",
"Articles.aspx{*pathInfo}",
null,
constraints: new { pathInfo = new LegacyDocumentRouteConstraint() }
);
route.RouteHandler = new RedirectRouteHandler();
This route merely captures any requests for /articles.aspx, but it has a constraint and a custom route handler.
The purpose of the constraint is to ensure we at least have the ID query string property and it is a number:
public class LegacyDocumentRouteConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
if (routeDirection == RouteDirection.UrlGeneration)
{
// Don't both doing anything while we generate links.
return false;
}
string id = httpContext.Request.QueryString["id"];
if (string.IsNullOrWhiteSpace(id))
{
// No query string argument was provided.
return false;
}
int documentId;
if (!int.TryParse(id, out documentId))
{
// The Id is not a number.
return false;
}
// Set the output document Id in the route values.
values["documentId"] = documentId;
return true;
}
}
If the Id was not provided, or was not a number, we can't match to an existing document, so the route will be skipped over. But when the constraint is satisfied, we store a variable in the route values values["documentId"] = documentId so we can then use it again (without having to parse it from the query string again) later in the route handler:
public class RedirectRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext context)
{
int documentId = (int)context.RouteData.Values["documentId"];
return new RedirectLegacyDocumentHttpHandler(documentId);
}
private class RedirectLegacyDocumentHttpHandler : IHttpHandler
{
private int _documentId;
public RedirectHttpHandler(int documentId)
{
_documentId = documentId;
}
public bool IsReusable { get { return false; } }
public void ProcessRequest(HttpContext context)
{
var response = context.Response;
string url = ResolveNewDocumentUrl();
if (url == null)
{
// Issue a 410 to say the document is no longer available?
response.StatusCode = 410;
}
else
{
// Issue a 301 to the new location.
response.RedirectPermanent(url);
}
}
public string ResolveNewDocumentUrl()
{
// Resolve to a new url using _documentId
}
}
}
Route handlers perform the logic of mapping from ASP.NET routing back into the IHttpHandler logic of the ASP.NET runtime. In normal MVC, this would map to the standard MvcHandler which invokes controllers, but in our case, we need only to issue a redirect.
In the route handler, we grab our document Id from the route values, and create a new HTTP handler which performs the actual redirect. You'll need to plumb in the bit which you resolve what the actual new url would be (ResolveNewDocumentUrl), but generally it will resolve the url, if the url comes back as null, we'll issue a HTTP 410 Gone response to say to clients (and more importantly crawlers) that the item is no longer there, or it will issue an HTTP 301 Permanent Redirect with the appropriate location header to the new url.
I had overcome this by creating an xml file on the server with the below schema
<URLMapper>
<Code>1</Code>
<OLDURL>%Oldurl.aspx%</OLDURL>
<NEWURL>default.aspx</NEWURL>
<PermanentRedirect>true</PermanentRedirect>
<Order>1</Order>
<Status>true</Status>
</URLMapper>
Loaded this in Application_Start Event to an application variable (in the form of a datatable).
And in the Begin Request --
void Application_BeginRequest(object sender, EventArgs e)
{
if (Application["URLMapper"] == null) return;
DataTable dtURLs = Application["URLMapper"] as DataTable;
if (dtURLs.Rows.Count == 0) return;
string OrigUrl = HttpContext.Current.Request.Url.ToString().ToLower().Replace("'", "`");
DataRow[] drFound = dtURLs.Select("Status = true and '" + OrigUrl.Trim() + "' like oldurl", "Order",DataViewRowState.CurrentRows);
if (drFound.Length == 0) return;
string OldURL = drFound[0]["OldURL"].ToString().Replace("%","");
Response.RedirectPermanent(OrigUrl.Replace(OldURL, drFound[0]["NewURL"].ToString().Trim()), true);
return;
}

ASP.NET MVC 5 Attribute Routing: Url.Action returns null

I am experiencing an issue with a refactoring of our payment processing action method (called by our 3rd-party online payment provider). We have a product controller with the [Authorize] and [RoutePrefix("products")] attributes at the class level, and action methods including the following:
Product(string contractNumber) with routing attribute [Route("{productCode}")]
MakePayment(string productCode, PaymentAmountType? amountSelection, decimal? amountValue) with routing attribute [Route("{productCode}")] and [HttpPost] attribute
ProcessPayment(string productCode, string result) with routing attribute [Route("{productCode}")]
Because our payment gateway needs to be able to call our ProcessPayment action before the visitor is redirected to that same URL, we've had to refactor that to a separate controller without the [Authorize] attribute. (We already have mechanisms to prevent double-crediting a payment.)
Before this refactoring, the MakePayment action method correctly formulated the correct return URL in the following call to Url.Action():
var rawCallbackUrl = Url.Action("ProcessPayment", new { productCode = productCode });
The ProcessPayment action method has now been moved out of the product controller and into a new controller, ExternalCallbackController, which has no attributes (let alone [Authorize]), in order to avoid having an HTTP 401 response returned to the payment provider.
The route attribute on ProcessPayment is now [Route("order-processing/{productCode}/process-payment")] to avoid clashing with the RoutePrefix on the product controller. All references to this updated action method are updated to specify the ExternalCallbackController.
Manually browsing to the URL causes the breakpoint set inside ProcessPayment to be hit, so the route apparently works successfully.
The problem is that in MakePayment, the following call returns null:
var rawCallbackUrl = Url.Action("ProcessPayment", "ExternalCallback", new { productCode = productCode });
Given that I am specifying both the controller and action method, why is Url.Action(...) not returning the expected URL in the form order-processing/{productCode}/process-payment?
From Day 1, our RegisterRoutes() method in RouteConfig has had attribute routing properly initialised with
routes.MapMvcAttributeRoutes();
How can I get the correct URL returned from the call to Url.Action(...)?
Doh - I've figured out what went wrong. Despite the sanitising of names in the source code (which were specific to our client), it turns out that there was a mismatch in the following call:
var rawCallbackUrl = Url.Action("ProcessPayment", "ExternalCallback", new { productCode = productCode });
and the ProcessPayment() action method.
This is akin to the following (note the use of productNumber instead of productCode):
var rawCallbackUrl = Url.Action("ProcessPayment", "ExternalCallback", new { productNumber = productNumber });
trying to reference the action method:
[Route("order-processing/{productCode}/process-payment")]
public ActionResult ProcessPayment(string productCode, string result)
{
...
}
It also turns out that I can also use the same prefix "products" instead of "order-processing", as MVC creates one Route per attribute route in the routing table. Hope that helps others stuck in a similar situation.
I got this Error : Url.Action return null.
public async Task<IActionResult> ForgotPassword(ForgotPassword forgotPassword)
{
if (ModelState.IsValid)
{
// Find the user by email
var user = await userManager.FindByEmailAsync(forgotPassword.Email);
// If the user is found AND Email is confirmed
if (user != null && await userManager.IsEmailConfirmedAsync(user))
{
// Generate the reset password token
var token = await userManager.GeneratePasswordResetTokenAsync(user);
// Build the password reset link
var passwordResetLink = Url.Action("ResetPassword", "Account",
new { email = forgotPassword.Email, token }, Request.Scheme);
ViewBag.PRL = passwordResetLink;
// Send the user to Forgot Password Confirmation view
return View("ForgotPasswordConfirmation");
}
return View("ForgotPasswordConfirmation");
}
return View(forgotPassword);
}

A simple POST request to Web API not hitting the API at all

From my MVC application, I am trying to make a POST request to these sample end-points (actions) in an API controller named MembershipController:
[HttpPost]
public string GetFoo([FromBody]string foo)
{
return string.Concat("This is foo: ", foo);
}
[HttpPost]
public string GetBar([FromBody]int bar)
{
return string.Concat("This is bar: ", bar.ToString());
}
[HttpPost]
public IUser CreateNew([FromBody]NewUserAccountInfo newUserAccountInfo)
{
return new User();
}
Here's the client code:
var num = new WebAPIClient().PostAsXmlAsync<int, string>("api/membership/GetBar", 4).Result;
And here's the code for my WebAPIClient class:
public class WebAPIClient
{
private string _baseUri = null;
public WebAPIClient()
{
// TO DO: Make this configurable
_baseUri = "http://localhost:54488/";
}
public async Task<R> PostAsXmlAsync<T, R>(string uri, T value)
{
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(_baseUri);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml"));
var requestUri = new Uri(client.BaseAddress, uri);
var response = await client.PostAsXmlAsync<T>(requestUri, value);
response.EnsureSuccessStatusCode();
var taskOfR = await response.Content.ReadAsAsync<R>();
return taskOfR;
}
}
}
I have the following default route defined for the Web API:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
UPDATE
My code breaks into the debugger until the time the PostAsXmlAsync method on the System.Net.HttpClient code is called. However, no request shows up in Fiddler.
However, if I try to compose a POST request in Fiddler or try to fire a GET request via the browser to one of the API end-points, the POST request composed via Fiddler tells me that I am not sending any data and that I must. The browser sent GET request rightly tells me that the action does not support a GET request.
It just seems like the System.Net.HttpClient class is not sending the POST request properly.
One of the most usual problems is that you don't use the appropriate attribute.
Take into account that there are attributes for ASP.NET MVC and ASP.NET Web API with the same name, but which live in different namespaces:
For Web API you must use the one in System.Web.Http
For MVC, the one in System.Web.MVc
This is a very very usual error, and it affects to allkind of things that exist for both MVC and Web API. So you must be very careful when using something which can exists in bith worlds (for example filters, attributes, or dependency injection registration).
I experienced a similar problem (may not be same one though). In my case, I hadn't given name attribute to the input element. I only figured that out when fiddler showed no post data being sent to the server (just like your case)
<input id="test" name="xyz" type="text" />
Adding the name attribute in the input tag fixed my problem.
However, there is one more thing to note. WebAPI does not put form data into parameters directly. Either you have to create an object with those properties and put that object in the parameter of the post controller. Or you could put no parameters at all like this:
[Route("name/add")]
public async Task Post()
{
if (!Request.Content.IsMimeMultipartContent())
{
return;
}
var provider = PostHelper.GetMultipartProvider();
var result = await Request.Content.ReadAsMultipartAsync(provider);
var clientId = result.FormData["xyz"];
...
Try changing the FromBody to FromUri.
If the parameter is a "simple" type, Web API tries to get the value from the URI. Simple types include the .NET primitive types (int, bool, double, and so forth), plus TimeSpan, DateTime, Guid, decimal, and string, plus any type with a type converter that can convert from a string.
For complex types, Web API tries to read the value from the message body, using a media-type formatter.
Remove FromBody at all and don't make any restrictions in passing parameters (it can be passed at this time either in uri, query string or form submissions (which is kinda a similar to query strings)
[HttpPost]
public string GetFoo(string foo){...}
It will be implicitly parsed and passed.

Resources