Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature request: strong typed results #76

Open
RoyalBjorn opened this issue Apr 20, 2021 · 9 comments
Open

Feature request: strong typed results #76

RoyalBjorn opened this issue Apr 20, 2021 · 9 comments

Comments

@RoyalBjorn
Copy link

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
Copy link
Owner

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

@RoyalBjorn
Copy link
Author

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
Copy link
Owner

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
Copy link
Author

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
Copy link
Owner

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
Copy link
Author

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
Copy link
Owner

Ok: sounds good!

@admalledd
Copy link

admalledd commented Apr 19, 2022

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
Copy link
Owner

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants