zijianhuang / webapiclientgen

Strongly Typed Client API Generators generate strongly typed client APIs in C# .NET and in TypeScript for jQuery and Angular 2+ from ASP.NET Web API and .NET Core Web API
MIT License
168 stars 38 forks source link

Issue when using your code generator when there are multiple [Route ()] attributes #105

Closed KeithVinson closed 3 years ago

KeithVinson commented 3 years ago

I ran across this issue and I was wondering if you knew of a work around for it. In an asp.net web API2 project I ran across this issue (from within one of the controllers): [Route ("HotLinkV3/Status")] [Route ("HotLinkV4/Status")] [Route ("ContentServerV1/Status")] [Route ("Dmca108V1/Status")] [HttpGet] public string StatusCheck () { return "OK"; } This kind of thing was done to support multiple generations of clients, each of which are using one of the 4 different routes. They all end up at the same controller method. The issue arises when I try and use your tool webapiclientgen. the generated class ends us with 4 methods all called StatusCheck each one has in it one of the four API routes HotLinkV3/Status, HotLinkV4/Status, etc. embedded with in it. However because the 4 methods all have the same method signature, like this: public string StatusCheck() { var requestUri = "HotLinkV3/Status"; using (var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, requestUri)) { var responseMessage = client.SendAsync(httpRequestMessage).Result; try { responseMessage.EnsureSuccessStatusCode(); var stream = responseMessage.Content.ReadAsStreamAsync().Result; using (JsonReader jsonReader = new JsonTextReader(new System.IO.StreamReader(stream))) { return jsonReader.ReadAsString(); } } finally { responseMessage.Dispose(); } } } vs. public string StatusCheck() { var requestUri = "HotLinkV4/Status"; using (var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, requestUri)) { var responseMessage = client.SendAsync(httpRequestMessage).Result; try { responseMessage.EnsureSuccessStatusCode(); var stream = responseMessage.Content.ReadAsStreamAsync().Result; using (JsonReader jsonReader = new JsonTextReader(new System.IO.StreamReader(stream))) { return jsonReader.ReadAsString(); } } finally { responseMessage.Dispose(); } } }

As you are well aware the compiler really doesn't like this, for obvious reasons... Any Ideas as to how to best handle this situation? Thanks for your feedback.

zijianhuang commented 3 years ago

It is an interesting subject that I had considered in the very early days of the development around 5 years ago, and this is down to 2 major areas:

  1. The business intent of this project. Client developers do not need to care about the technical details of HTTP, like route and HTTP method, and focus on the API prototype like in-process API, as further described in Points of Interests at https://www.codeproject.com/Articles/1074039/Generate-Csharp-Client-API-for-ASP-NET-Web-API
  2. Versioning of API

In your OP, you might have hinted a technical solution of "improved" webapiclientgen: generate multiple client API functions for one Web API with multiple routes. For example, client functions with suffix named after each route.

Another approach to deal with versioning is:

  1. Keep the practice of one route+parameters one Web API function.
  2. Keep one version of API in one controller.

I would prefer this approach since the whole design of WebApiClientGen is optimized for RPC, not REST, and I consider that RPC is more suitable for complex business applications with complex business models, while REST is too technical, verbose and even wasteful in such contexts.

And generally I would group functions in a controller based on respective business model, not REST resources.

[Route("api/[controller]")]
public class HotLinkV3Controller : ControllerBase
{
[HttpGet, Route("status")]]
public string StatusCheck ()
{
return "OK";
}
... other Web API functions....
}

[Route("api/[controller]")]
public class HotLinkV3Controller : ControllerBase
{
[HttpGet, Route("status")]
public string StatusCheck ()
{
return "OK";
}
... other Web API functions....
}

[Route("api/[controller]")]
public class HotLinkV4Controller : ControllerBase
{
[HttpGet]
public string StatusCheck ()
{
return "OK";
}
... other Web API functions....
}

[Route("api/[controller]")]
public class HotLinkV4Controller : ControllerBase
{
[HttpGet]
public string StatusCheck ()
{
return "OK";
}
... other Web API functions....
}

[Route("api/[controller]")]
public class ContentServerV1Controller : ControllerBase
{
[HttpGet]
public string StatusCheck ()
{
return "OK";
}
... other Web API functions....
}

And in the client codes, you will have

HotLinkV3Client ...
HotLinkV4Client ...
ContentServerV1Client

I believe that in some contexts, especially in complex business applications, such approach minimize the maintenance cost on server development and client development, with versioning.

And presumably you generally have just 1 to 3 statements in each Web API function. For example:

    [Route("api/[controller]")]
    public class PeopleController : ControllerBase
    {
        [HttpPost]
        [Route("Add")]
        public Guid Add([FromBody] Person entity)
        {
            return PersonOperations.Add(entity);
        }

        [HttpPost]
        [Route("Undelete")]
        public void Undelete([FromQuery] Guid id)
        {
            EntityOperations.Reactivate(id);
        }

        [HttpDelete]
        [Route("ForceDelete")]
        public bool ForceDelete([FromQuery] Guid id)
        {
            return PersonOperations.DeleteInTransaction(id);
        }

        [HttpPut]
        [Route("Update")]
        public int Update([FromBody] Person entity)
        {
            return PersonOperations.Update(entity);
        }

        [HttpGet]
        [Route("Get")]
        public Person Get([FromQuery] Guid id)
        {
            return PersonOperations.Get(id);
        }

        [HttpGet]
        [Route("GetName")]
        public string GetName([FromQuery] Guid id)
        {
            return PersonOperations.GetName(id);
        }

So the cost of versioning with the preferred approach could be minimum. And also, if you would provide SOAP/WCF endpoints, the development is obviously easy. WCF or WebAPI controller is just something of transportation, so the implementation of Web API functions should be thin, and the implementation details should be hidden behind something like PersonOperations. And such approach is good for unit testing too.

And in complex business applications, generally I don't explicitly return HTTP (error) status in the controller, and I throw exceptions inside something like PersonOperations, and then the filter or error handling middleware will catch the exception and return HTTP status code.

zijianhuang commented 3 years ago

workaround or solution available as answered