twitchax / AspNetCore.Proxy

ASP.NET Core Proxies made easy.
MIT License
505 stars 80 forks source link

Feature request: strong typed results #76

Open RoyalBjorn opened 3 years ago

RoyalBjorn commented 3 years ago

Is it possible to support strong typed results (IActionResult). I'm using 'WithAfterReceive' to rewrite the response. This is working well, but I would like to be able to use an ActionResult instead of 'Task', because then generation of Api documentation and other features would be supported better.

twitchax commented 3 years ago

Can you give me a code snippet example of what you're trying to do?

RoyalBjorn commented 3 years ago

Below you can find an example of how (just a proof of concept) I used AspNetCore.Proxy to create sort of an api gateway that translates the incoming and outgoing messages. As you can see the happy path returns a 'PartStockInfo' object. I would like the return type of my method to be Task<IActionResult> instead of Task, but when I change this, I get an error at the last line of the code where HttpProxyAsync is called, because it just returns a Task.

        /// <summary>
        /// Route for retrieving stock info for a given part number.
        /// </summary>
        /// <param name="partid">The identification of the part.</param>
        /// <returns>Stock information for the given part in case the part is a known part.
        /// Otherwise, a 404 error code is returned.</returns>
        [HttpGet]
        [Produces("application/json")]
        [ProducesResponseType(typeof(PartStockInfo), StatusCodes.Status200OK)]
        [ProducesResponseType(typeof(Problem), StatusCodes.Status400BadRequest)]
        [ProducesResponseType(typeof(Problem), StatusCodes.Status404NotFound)]
        [Route("api/part/{partid}/stock/")]
        public Task GetPartStock([FromRoute] string partid) 
        {
            var options = HttpProxyOptionsBuilder.Instance
                .WithBeforeSend((context, requestmessage) => {
                    // This is the moment to change details in the call to the backend.
                    requestmessage.Headers.Accept.Clear();
                    requestmessage.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
                    return Task.CompletedTask;
                })
                .WithAfterReceive(async (context, responsemessage) =>
                {
                    if(responsemessage.IsSuccessStatusCode)
                    {
                        // Todo: translate received xml response into json respons with different structure.
                        string content = await responsemessage.Content.ReadAsStringAsync();
                        var stockInfo = new PartStockInfo()
                        {
                            PartId = partid,
                            StockLevel = StockLevelIndicator.SUPERSEDED,
                            Successor = "100016"
                        };

                        responsemessage.Content = new StringContent(JsonSerializer.Serialize(stockInfo), System.Text.Encoding.UTF8, "application/json");
                        return;
                    }

                    var problem = new Problem() 
                    {
                        Instance = Request.Path,
                        Status = (int)responsemessage.StatusCode,
                    };

                    if(responsemessage.StatusCode == System.Net.HttpStatusCode.NotFound) 
                    {
                        problem.Title = $"The requested item ({partid}) cannot be found.";
                    }

                    responsemessage.Content = new StringContent(JsonSerializer.Serialize(problem), System.Text.Encoding.UTF8, "application/json");
                    return;
                })
                // Use the named http client with ntlm authentication.
                .WithHttpClientName(Startup.HTTP_NTLM_CLIENT_NAME)
                .WithShouldAddForwardedHeaders(false)
                .Build();

            // Call the backend
            return this.HttpProxyAsync($"{config.Backend.BaseUrl}/Integration_Customer_Card('{partid}')", options);
        }

Please do tell me if what I'm trying to do (translating response messages) is not the intended use of AspNetCore.Proxy.

twitchax commented 3 years ago

Interesting: this might be possible, but it is not trivial since ASP.NET will try to pick up that return value and serialize it into the response. This library already does that by default from the proxied endpoint.

What is the scenario that this would enable? You already have [ProducesResponseType(typeof(PartStockInfo), StatusCodes.Status200OK)], so I'm wondering what changing the return type would do for you.

RoyalBjorn commented 3 years ago

Well, for starters it would remove the need to add the response type to the 'ProducesResponse' attribute. But my main motiviation is that I like to use strong typed responses as much as possible because of the design time validation in the IDE. But as you already said, it is not a trivial feature.

twitchax commented 3 years ago

So, hmmm, I have a few ideas. It might be a bit complex, but I might be able to have WithAfterReceive optionally return a Task<T>, and then, if it does, I skip writing to the response.

Another option would be to create a new method for this specific purpose.

RoyalBjorn commented 3 years ago

Thanks for your effort @twitchax! In my opinion a generic 'overload' might be the best option because of compatibility with the existing version. If I can find some time, I'll fork your repository and try something myself.

twitchax commented 3 years ago

Ok: sounds good!

admalledd commented 2 years ago

Going to +1 this, but with a slightly different use case: I have a need to conditionally proxy for legacy API compatibility reasons.

EDIT: further RTFM has me finding that I think await this.HttpProxyAsync(...); return new EmptyResult(); likely to do what I need to allow using further IActionResult stuff? Still playing with this and running against my unit tests...

As in something like this is a pattern I wish to use:

[HttpGet]
[Route("some/horrible/classic.asp")] //API pretends/acts alike to a legacy classic ASP "api"
public async Task<IActionResult> FooClasicCompat([FromQuery] string someParam, [FromQuery] string fileName, ...)
{
    if (someParam != null && someParam.Contains("legacy") // handwave: by some method "this needs to be proxied"
    {
        var legacyUrl = $"{this.GetLegacyUrlRoot}/some/horrible/classic.asp{this.Request.QueryString.Value}";
        return await this.HttpProxyAsync(legacyUrl); // DESIRED THING SOMEHOW
    }
    //else, do the thing here in AspNetCore land instead, which can return multiple other possible IActionResults etc:
   return File(this.FileActor.GetFileStream(fileName), this.MimeMap.GetMimeTypeByFileName(fileName); //or any other ActionResult impl/helpers
}

That for other developers on my team it is very nice to stay in "ActionResult helper land" if we can help it. Just mostly asking/wondering about a way to tell/mock over a dummy IActionResult so that nothing else is written to the Response.

twitchax commented 2 years ago

Ok, makes sense. I will have to think on this. :)