Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Provide a more in depth example of writing responses in middleware #7238

Closed
josephearl opened this issue Jan 12, 2018 · 15 comments
Closed

Provide a more in depth example of writing responses in middleware #7238

josephearl opened this issue Jan 12, 2018 · 15 comments

Comments

@josephearl
Copy link

I'd like to return a response from middleware that obeys the same behaviour as returning an ObjectResult from a controller, i.e. it uses my default output formatter if no Accept header is specified or uses the correct output formatter based on the Accept header.

Looking at the current docs it's not clear how this can be achieved since they only demonstrate writing simple strings in the response.

See https://stackoverflow.com/questions/48228167/how-can-i-write-a-response-in-middleware-using-the-same-settings-as-returning-an for a more in depth explanation.

@josephearl
Copy link
Author

Another user also asked for this in a comment on the page https://docs.microsoft.com/en-us/aspnet/core/mvc/models/formatting

Stephen.lautier Jul 10, 2017
Can you use content negotiation somehow inside a middleware? In order to support multiple output formats.
Thanks!

@khellang
Copy link
Contributor

There's no support for using MVC's content negotiation in middleware right now.

@rynowak has been doing some work to streamline the result executor model, so from 2.1, you should be able to inject IActionResultExecutor<ObjectResult> and call that. There's still an issue with constructing an ActionContext though.

See #6822 (comment) and aspnet/Diagnostics#346.

@mkArtakMSFT
Copy link
Member

Thanks for reporting this issue. We'll consider your feedback as part of an existing issue we're tracking as part of our planning at the moment.

@khellang
Copy link
Contributor

khellang commented Jan 13, 2018

In 2.1, you can have something like this:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;

public static class HttpContextExtensions
{
    private static readonly RouteData EmptyRouteData = new RouteData();

    private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor();

    public static Task WriteResultAsync<TResult>(this HttpContext context, TResult result)
        where TResult : IActionResult
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        var executor = context.RequestServices.GetService<IActionResultExecutor<TResult>>();

        if (executor == null)
        {
            throw new InvalidOperationException($"No result executor for '{typeof(TResult).FullName}' has been registered.");
        }

        var routeData = context.GetRouteData() ?? EmptyRouteData;

        var actionContext = new ActionContext(context, routeData, EmptyActionDescriptor);

        return executor.ExecuteAsync(actionContext, result);
    }
}

And use it like this:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;

public class Program : StartupBase
{
    public static Task Main(string[] args)
    {
        return BuildWebHost(args).RunAsync();
    }

    public static IWebHost BuildWebHost(string[] args)
    {
        return new WebHostBuilder().UseStartup<Program>().UseKestrel().Build();
    }

    public override void ConfigureServices(IServiceCollection services)
    {
        services.AddMvcCore().AddJsonFormatters();
    }

    public override void Configure(IApplicationBuilder app)
    {
        app.Use((ctx, next) =>
        {
            var model = new Person("Krisian", "Hellang");

            var result = new ObjectResult(model);

            return ctx.WriteResultAsync(result);
        });
    }
}

public class Person
{
    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }

    public string FirstName { get; }

    public string LastName { get; }
}

@josephearl
Copy link
Author

Thanks @khellang

@macwier
Copy link

macwier commented Jul 17, 2018

I've tried something like this, but it always fails on this (cannot resolve the service):

var executor = context.RequestServices.GetService<IActionResultExecutor<TResult>>();

I've tried resolving ObjectResultExecutor and same thing. Any idea? I can see the ObjectResultExecutor being registered in services after the call to .AddMvc().

@khellang
Copy link
Contributor

What is TResult?

@macwier
Copy link

macwier commented Jul 17, 2018

Well, in this case a class that derives from BadRequestObjectResult.
But as I said- I've also tried resolving ObjectResultExecutor directly (to remove the possibility of generic messing with the DI cointainer) and I get the same error. I've also tried to inject the ObjectResultExecutor to middleware via constructor or via method injection- same error (a bit different variants, but it get's down to the same thing).

Not sure if this matters, I'm using the latest versions of asp.netcore packages (Microsoft.AspNetCore 2.1.2, Microsoft.AspNetCore.Mvc 2.1.1).

@khellang
Copy link
Contributor

The services are registered as their IActionResultExecutor<T> implementation here:

services.TryAddSingleton<IActionResultExecutor<ObjectResult>, ObjectResultExecutor>();
services.TryAddSingleton<IActionResultExecutor<PhysicalFileResult>, PhysicalFileResultExecutor>();
services.TryAddSingleton<IActionResultExecutor<VirtualFileResult>, VirtualFileResultExecutor>();
services.TryAddSingleton<IActionResultExecutor<FileStreamResult>, FileStreamResultExecutor>();
services.TryAddSingleton<IActionResultExecutor<FileContentResult>, FileContentResultExecutor>();
services.TryAddSingleton<IActionResultExecutor<RedirectResult>, RedirectResultExecutor>();
services.TryAddSingleton<IActionResultExecutor<LocalRedirectResult>, LocalRedirectResultExecutor>();
services.TryAddSingleton<IActionResultExecutor<RedirectToActionResult>, RedirectToActionResultExecutor>();
services.TryAddSingleton<IActionResultExecutor<RedirectToRouteResult>, RedirectToRouteResultExecutor>();
services.TryAddSingleton<IActionResultExecutor<RedirectToPageResult>, RedirectToPageResultExecutor>();
services.TryAddSingleton<IActionResultExecutor<ContentResult>, ContentResultExecutor>();

So resolving ObjectResultExecutor won't help.

If the type derives from BadRequestObjectResult, you won't get a match, since TResult will be something that's not registered in the container. I.e. you're requesting IActionResultExecutor<MyBadRequestObjectResult> and there's no service registered for that.

I suggest changing the method to a non-generic version, which takes in an ObjectResult (or keep it generic, but constrain the generic parameter to ObjectResult) and resolve IActionResultExecutor<ObjectResult> directly.

@macwier
Copy link

macwier commented Jul 18, 2018

Doh, now when you said it, it's so obvious. Thanks, got it working.

For a side note, I did try a case when TResult was NotFoundObjectResult and it also didn't work.

@khellang
Copy link
Contributor

Yes, that's because the registration is for IActionResultExecutor<ObjectResult>. All types that derive from ObjectResult should use that 😊

@AeonDave
Copy link

What if we are handling global exception and we want to write a content in json or xml or custom (based on content negotiation)?
with the method
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
where there is no TResult.
How we can achieve this?

e.g.

public ErrorHandlingMiddleware(RequestDelegate next)
{
    _next = next;
}

public async Task Invoke(HttpContext context)
{
    try
    {
        await _next(context).ConfigureAwait(false);
    }
    catch (Exception ex)
    {
        await HandleExceptionAsync(context, ex).ConfigureAwait(false);
    }
}

private static Task HandleExceptionAsync(HttpContext context, Exception exception)
{
    //code that serialize with the correct serializer based on header "accept"
    return context.Response.WriteAsync(result);
}

@khellang
Copy link
Contributor

khellang commented Jul 27, 2018

See this middleware for an example of how to do this.

@AeonDave
Copy link

Thank you it's almost clear but as the example:

        private Task HandleExceptionAsync(HttpContext context, Exception ex)
        {
            var actionContext = new ActionContext(context, context.GetRouteData(), EmptyActionDescriptor);
            var result = new ObjectResult("Error")
            {
                StatusCode = (int)HttpStatusCode.InternalServerError,
            };
            return Executor.ExecuteAsync(actionContext, result);
        }

this code generates this error

System.InvalidOperationException: StatusCode cannot be set because the response has already started.

@bugproof
Copy link

bugproof commented Sep 18, 2018

It would be nice to have WriteResultAsync in the official library.

This should be documented more, there is a lot of wrong and misleading examples on the internet always writing to JSON(like using Newtonsoft.Json directly) completely ignoring content negotiation and output formatters mechanisms in ASP.NET Core.


@khellang can't one just use IContentNegotiator for this passing output formatters from IOptions<MvcOptions>?

From what I see IContentNegotiator is not used in IActionResultExecutor<ObjectResult> implementation and OutputFormatterSelector is used instead.

IContentNegotiator is used only in HttpRequestMessage.CreateResponse<T>

EDIT: actually IContentNegotiator is not even a part of ASP.NET Core but System.Net.Http and expects System.Net.Http.Formatting.MediaTypeFormatter instead of OutputFormatter we have in MVC so it's completely unrelated at this point and is a remain of classic ASP.NET and a part of Web API compat shim to help porting old ASP.NET apps to Core :)

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

No branches or pull requests

6 participants