1

I have only worked with a few Restful API's and have been in need of creating one. Ultimately I need an API and was hoping for the Restful aspect but that portion isn't the ultimate focus.

I created a prototype and had the basic html calls working fine. I then wanted to move to other types of calls. I've looked at a lot of articles and snippets but none of them seem to really answer the question for me. I'm using Postman to test. Right now I have two basic calls "Get" calls both with three string parameters realizing they have the same signature but different names. I get an error related to ambiguity. Any help appreciated

The following is in my setup:

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        var config = new HttpConfiguration();

        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        config.Routes.MapHttpRoute(
            name: "ApiByName",
            routeTemplate: "api/{controller}/{name}",
            defaults: null,
            constraints: new { name = @"^[a-z]+$" }
        );

        config.Formatters.Remove(config.Formatters.XmlFormatter);
        config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        config.Formatters.JsonFormatter.SerializerSettings.DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Utc;

        app.UseWebApi(config);
    }
}

The following are my two method calls

[Route("{UserId}/{Key}/{Source}", Name = "GetToken")]
[HttpGet]
public HttpResponseMessage GetToken(string UserId, string Key)
{
}

[Route("{Token}/{AcctNo}/{YearMonth}", Name = "GetInvoices")]
[HttpGet]
public HttpResponseMessage GetInvoices(string Token, string AcctNo, string YearMonth)
{
}

If I comment out one of these the other will get called but I can't seem to get it to work.

-------------Update------------...have modified the code as follows.

The "ApiByName" has been removed from the config startup. And below are the methods. I included a generic get that I hadn't shown before.

public IEnumerable<CustomListItem> Get()
{
    //            return new string[] { "value1", "value2" };
    return _listItems;
}

[Route("GetToken/{UserId}/{Key}/{Source}")]
[HttpGet]
public HttpResponseMessage GetToken(string UserId, string Key, string Source)
{
   ...
}

[Route("GetInvoices/{Token}/{AcctNo}/{YearMonth}")]
[HttpGet]
public HttpResponseMessage GetInvoices(string Token, string AcctNo, string YearMonth)
{
   ...
}

Even with above changes calls always go to the generic Get (First method shown) Following are examples of calls from postman (as well as pasted directly in browser)??

http://localhost:37788/api/listitems/GetToken?UserId=Dshadle&Key=ABC&Source=Postman

http://localhost:37788/api/listitems/GetInvoices?Token=abc&AcctNo=123&YearMonth=2019/12

haldo
  • 14,512
  • 5
  • 46
  • 52
DaleS
  • 23
  • 4
  • How will you call these actions? What I mean is, do you want them to have different routes? Or do you expect both to be accessible from the exact same route/URL? – haldo Jan 29 '20 at 00:20
  • I did want them to be different routes. I've modified the code to use ActionRoute (shown above) but it still not working. – DaleS Jan 29 '20 at 17:29

3 Answers3

1

It looks like you want to map api/ListItems/GetToken to the GetToken() endpoint, and similarly for GetInvoices. From the URLs you've shared I'll assume your controller is named ListItems.

There are actually two ways to accomplish this. Whichever method you choose to use, you do not need to add the parameters to the route for the URLs you showed.

1. Attribute Routing

Remove the ApiByName route from the config. Then you can either decorate each action with the full path (eg [Route("api/ListItems/GetToken")]), or decorate the controller with [RoutePrefix("api/ListItems")] and decorate the action(s) with Route("MyAction"). The example below uses RoutePrefix on the controller.

[RoutePrefix("api/ListItems")]
public class ListItemsController : ApiController
{     

  [Route("GetToken")]
  [HttpGet]
  public HttpResponseMessage GetToken(string UserId, string Key, string source)

  [Route("GetInvoices", Name = "GetInvoices")]
  [HttpGet]
  public HttpResponseMessage GetInvoices(string Token, string AcctNo, string YearMonth)

If you want a URL that looks like ListItems/GetToken/123/ABC/Postman then just add the parameters back into the route [Route("GetToken/{UserId}/{Key}/{Source}")], however, the URLs you posted do not match that format.

The Name property of the Route attribute may not work like you expect. It's actually used to define the endpoint internally to the API, not externally. For example, RedirectToRoute("GetInvoices") would return a redirect to the GetInvoices endpoint. It's up to you whether you need to include it or not.

2. Conventional Routing

Do not use the [Route] attribute at all.

Instead, add the following routes to the API route config (thanks to Darin Dimitrov's answer):

config.Routes.MapHttpRoute(
     name: "ApiById",
     routeTemplate: "api/{controller}/{id}",
     defaults: new { id = RouteParameter.Optional },
     constraints: new { id = @"^[0-9]+$" }
 );

config.Routes.MapHttpRoute(
    name: "ApiByAction",
    routeTemplate: "api/{controller}/{action}",
    defaults: new { action = "Get" }
);

Then in the controller use the ActionName("MyAction") attribute:

[HttpGet]
[ActionName("GetToken")]
public HttpResponseMessage GetToken(string UserId, string Key, string source)

[HttpGet]
[ActionName("GetInvoices")]
public HttpResponseMessage GetInvoices(string Token, string AcctNo, string YearMonth)

I have tested both of these methods with the following URLs:

// get list of items
http://localhost:28092/api/ListItems    

// get single item 
http://localhost:28092/api/ListItems/1  

// GetToken 
http://localhost:28092/api/ListItems/GetToken?userid=123&key=ABC&source=Postman

// GetInvoices
http://localhost:28092/api/ListItems/GetInvoices?token=ABC&acctno=1234&yearmonth=2019-12

Note 1: I would recommend not using / as a date separator in the URL since it could interfere with the routing, use - or another separator instead.

Note 2: The parameters must have a value or the framework will not be able to find the correct action. If (some of) the parameters should be optional, then specify default values in the method signature, eg GetToken(string UserId, string Key, string source = "") (source is optional here).


However, if you want to follow a RESTful pattern, then you should really structure your endpoints to match the resource(s) they act on/for. You should probably have an Invoice controller, where you can perform Get, Post, Put, Delete, etc for the Invoices. GetToken seems like it belongs in a different controller, unless it's somehow related to invoices.

haldo
  • 14,512
  • 5
  • 46
  • 52
  • Thanks for the feedback. I agree the Token should be in it's own controller and can do that but for the moment ignore that and just consider the two method calls. I had read, as you indicated, that the Name doesn't affect the external URL and I was originally thinking it would. That said however I've re-read the MS docs on it and modified it accordingly as you have shown above and it still won't goto to the correct end point. I've also tried adding and removing the name property as well as adding [ActionName] and all still end up going to the basic Get that I have..so frustrating. – DaleS Jan 29 '20 at 17:12
  • Thanks for all the work haldo and others. I also ended up finding Darin Dimitrov's post upon further surching. Added the config change, removed the Route and Get referernces and added the ActionName and it did the trick. I was going to post update but you beat me too it :) Thanks again for the help all! – DaleS Jan 29 '20 at 22:09
  • As far as proper Restful, Yes, this was a prototype merely to get the potential gotchas flushed out. Agree the token belongs in it's own controller. Invoice along with a few other methods by itself. – DaleS Jan 29 '20 at 22:16
0

If you're encountering this problem many times it is because you're putting too many actions into a single endpoint. Remember to separate the actions of each entity (or aggregate) into its own controller.

If that's not your case, then the simplest solution is to use query parameters

Put all 6 parameters in a model, pass it as a parameter for the Get method, annotated with [FromQuery].

Like this:

[HttpGet] 
public async Task<IActionResult> Get([FromQuery] GetRequestParameters parameters)
{
   ... 
}

That way you could call it like this:

GET https://your_url/api/your_controller?userId=123&key=secret&source=somesource

Or

GET https://your_url/api/your_controller?token=abcd&accountNumber=secret&yearMonth=202001

Beware though, you can receive nonsense parameters, like token, key and userId together, which do not match your requirements, so you should validate the input

  • Presumably, `GetToken` returns a different payload to `GetInvoices`. If they are merged into a single endpoint, how will consumers of the API handle this? In any case, this doesn't seem to follow a RESTful pattern IMO. – haldo Jan 29 '20 at 01:05
  • It is indeed not ideal, that's why I initially suggested it might be a problem with the design, maybe separate token and invoice into different endpoints? That sounds like a more reasonable flow, like Token/Get, then Invoice/Get with your acquired token. Of course this is just speculation given I don't have any knowledge of your application – Jorge Santiago Jan 29 '20 at 01:28
0

Your last approach is correct but the problem here is you're mixing 2 schemes.

You provide this setting: [Route("GetInvoices/{Token}/{AcctNo}/{YearMonth}")]

And then you turn around and test using query params like this

/api/ListItems/GetInvoices?token=ABC&acctno=1234&yearmonth=2019-12

But if you look at your Route attribute, it indicates that the route for this action should be done in the path. Instead, change the URL you're testing to match the path like so:

/api/ListItems/GetInvoices/ABC/1234/2019-12

McAden
  • 13,714
  • 5
  • 37
  • 63
  • I doubt that will work without further configuration, have you tested it? Also it looks like you copied the URLs from my answer not from the question :) – haldo Jan 29 '20 at 21:03
  • I did screw up copy/paste, you're right. However, the rest of my answer is pretty standard usage of `RouteAttribute`. The only URL difference between what I copied from your post by accident would be the format of `YearMonth` because of the `/` in the OP's would have issues in URL pathing. – McAden Jan 29 '20 at 21:09