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

d_mcg picture d_mcg · Mar 2, 2015 · Viewed 9.3k times · Source

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(...)?

Answer

d_mcg picture d_mcg · Mar 2, 2015

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.