web-dev-qa-db-fra.com

Comment lire le corps de la demande dans un contrôleur web aspi central?

J'essaie de lire le corps de la requête dans la méthode OnActionExecuting, mais j'obtiens toujours null pour le corps.

var request = context.HttpContext.Request;
var stream = new StreamReader(request.Body);
var body = stream.ReadToEnd();

J'ai essayé de définir explicitement la position du flux sur 0 mais cela ne fonctionnait pas non plus. Puisque c'est ASP.NET CORE, les choses sont peu différentes, je pense. Je peux voir tous les exemples ici en référence aux anciennes versions de webapi.
Y a-t-il une autre façon de faire cela?

62
Kasun Koswattha

Dans ASP.Net Core, il semble compliqué de lire la requête body plusieurs fois. Toutefois, si vous tentez votre première tentative correctement, les choses devraient bien se passer.

J'ai lu plusieurs redressements, par exemple en substituant le flux corporel, mais je pense que ce qui suit est le plus propre:

Les points les plus importants étant

  1. faire savoir à la demande que vous lirez son corps deux fois ou plus,
  2. ne pas fermer le flux corporel, et
  3. pour revenir à sa position initiale afin que le processus interne ne soit pas perdu.

[MODIFIER]

Comme le souligne Murad, vous pouvez également tirer parti de l’extension .Net Core 2.1: EnableBuffering Il stocke les demandes volumineuses sur le disque au lieu de le conserver en mémoire, évitant ainsi les problèmes de flux importants stockés en mémoire ( fichiers, images, ...). Vous pouvez modifier le dossier temporaire en définissant la variable d’environnement ASPNETCORE_TEMP. Les fichiers sont supprimés une fois la demande terminée.

Dans un AuthorizationFilter , vous pouvez effectuer les opérations suivantes:

// Helper to enable request stream rewinds
using Microsoft.AspNetCore.Http.Internal;
[...]
public class EnableBodyRewind : Attribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var bodyStr = "";
        var req = context.HttpContext.Request;

        // Allows using several time the stream in ASP.Net Core
        req.EnableRewind(); 

        // Arguments: Stream, Encoding, detect encoding, buffer size 
        // AND, the most important: keep stream opened
        using (StreamReader reader 
                  = new StreamReader(req.Body, Encoding.UTF8, true, 1024, true))
        {
            bodyStr = reader.ReadToEnd();
        }

        // Rewind, so the core is not lost when it looks the body for the request
        req.Body.Position = 0;

        // Do whatever work with bodyStr here

    }
}



public class SomeController : Controller
{
    [HttpPost("MyRoute")]
    [EnableBodyRewind]
    public IActionResult SomeAction([FromBody]MyPostModel model )
    {
        // play the body string again
    }
}

Ensuite, vous pouvez utiliser le corps à nouveau dans le gestionnaire de demandes.

Dans votre cas, si vous obtenez un résultat nul, cela signifie probablement que le corps a déjà été lu à une étape antérieure. Dans ce cas, vous devrez peut-être utiliser un middleware (voir ci-dessous).

Cependant, soyez prudent si vous manipulez des flux volumineux, ce comportement implique que tout soit chargé en mémoire, cela ne devrait pas être déclenché en cas de téléchargement de fichier.

Vous voudrez peut-être l'utiliser comme middleware

Le mien ressemble à ceci (encore une fois, si vous téléchargez/téléversez des fichiers volumineux, vous devez le désactiver pour éviter les problèmes de mémoire):

public sealed class BodyRewindMiddleware
{
    private readonly RequestDelegate _next;

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

    public async Task Invoke(HttpContext context)
    {
        try { context.Request.EnableRewind(); } catch { }
        await _next(context);
        // context.Request.Body.Dipose() might be added to release memory, not tested
    }
}
public static class BodyRewindExtensions
{
    public static IApplicationBuilder EnableRequestBodyRewind(this IApplicationBuilder app)
    {
        if (app == null)
        {
            throw new ArgumentNullException(nameof(app));
        }

        return app.UseMiddleware<BodyRewindMiddleware>();
    }

}
87
Jean

Pour pouvoir revenir en arrière dans le corps de la requête, la réponse de @ Jean m'a aidée à trouver une solution qui semble bien fonctionner. J'utilise actuellement ceci pour le gestionnaire de logiciel d'exception global, mais le principe est le même.

J'ai créé un middleware qui permet fondamentalement le rembobinage sur le corps de la demande (au lieu d'un décorateur).

using Microsoft.AspNetCore.Http.Internal;
[...]
public class EnableRequestRewindMiddleware
{
    private readonly RequestDelegate _next;

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

    public async Task Invoke(HttpContext context)
    {
        context.Request.EnableRewind();
        await _next(context);
    }
}

public static class EnableRequestRewindExtension
{
    public static IApplicationBuilder UseEnableRequestRewind(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<EnableRequestRewindMiddleware>();
    }
}

Ceci peut alors être utilisé dans votre Startup.cs comme ceci:

[...]
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    [...]
    app.UseEnableRequestRewind();
    [...]
}

En utilisant cette approche, j'ai pu rembobiner le flux de corps de la demande avec succès.

15
SaoBiz

Une solution plus claire, fonctionne dans ASP.Net Core 2.1.

Classe de filtre

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.Mvc.Filters;

public class ReadableBodyStreamAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        context.HttpContext.Request.EnableRewind();
    }
}

Dans un contrôleur

[HttpPost]
[ReadableBodyStream]
public string SomePostMethod()
{
    using (StreamReader stream = new StreamReader(HttpContext.Request.Body))
    {
        string body = stream.ReadToEnd();
        // body = "param=somevalue&param2=someothervalue"
    }
}
9
Andriod

La méthode IHttpContextAccessor fonctionne si vous souhaitez emprunter cette voie.

TLDR;

  • Injecter le IHttpContextAccessor

  • Rewind - HttpContextAccessor.HttpContext.Request.Body.Seek(0, System.IO.SeekOrigin.Begin);

  • Lire - System.IO.StreamReader sr = new System.IO.StreamReader(HttpContextAccessor.HttpContext.Request.Body); JObject asObj = JObject.Parse(sr.ReadToEnd());

Plus - Un exemple concis, non compilant, des éléments que vous devez vous assurer sont en place pour vous permettre de utilisable IHttpContextAccessor. Les réponses ont indiqué correctement que vous devrez revenir au début lorsque vous essayez de lire le corps de la demande. Les propriétés CanSeek, Position du flux de corps de la requête sont utiles pour le vérifier.

.NET Core DI Docs

// First -- Make the accessor DI available
//
// Add an IHttpContextAccessor to your ConfigureServices method, found by default
// in your Startup.cs file:
// Extraneous junk removed for some brevity:
public void ConfigureServices(IServiceCollection services)
{
    // Typical items found in ConfigureServices:
    services.AddMvc(config => { config.Filters.Add(typeof(ExceptionFilterAttribute)); });
    // ...

    // Add or ensure that an IHttpContextAccessor is available within your Dependency Injection container
    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}

// Second -- Inject the accessor
//
// Elsewhere in the constructor of a class in which you want
// to access the incoming Http request, typically 
// in a controller class of yours:
public class MyResourceController : Controller
{
    public ILogger<PricesController> Logger { get; }
    public IHttpContextAccessor HttpContextAccessor { get; }

    public CommandController(
        ILogger<CommandController> logger,
        IHttpContextAccessor httpContextAccessor)
    {
        Logger = logger;
        HttpContextAccessor = httpContextAccessor;
    }

    // ...

    // Lastly -- a typical use 
    [Route("command/resource-a/{id}")]
    [HttpPut]
    public ObjectResult PutUpdate([FromRoute] string id, [FromBody] ModelObject requestModel)
    {
        if (HttpContextAccessor.HttpContext.Request.Body.CanSeek)
        {
            HttpContextAccessor.HttpContext.Request.Body.Seek(0, System.IO.SeekOrigin.Begin);
            System.IO.StreamReader sr = new System.IO.StreamReader(HttpContextAccessor.HttpContext.Request.Body);
            JObject asObj = JObject.Parse(sr.ReadToEnd());

            var keyVal = asObj.ContainsKey("key-a");
        }
    }
}    
1
Randy Larson

J'ai eu un problème similaire lors de l'utilisation d'ASP.NET Core 2.1:

  • J'ai besoin d'un middleware personnalisé pour lire les données POSTed et effectuer certaines vérifications de sécurité.
  • l'utilisation d'un filtre d'autorisation n'est pas pratique, en raison du grand nombre d'actions affectées.
  • Je dois autoriser la liaison d'objets dans les actions ([FromBody] someObject). Merci à SaoBiz d’avoir signalé cette solution.

La solution évidente est donc de permettre à la demande d'être rembobinable, mais assurez-vous qu'après la lecture du corps, la liaison fonctionne toujours.

EnableRequestRewindMiddleware

public class EnableRequestRewindMiddleware
{
    private readonly RequestDelegate _next;

    ///<inheritdoc/>
    public EnableRequestRewindMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public async Task Invoke(HttpContext context)
    {
        context.Request.EnableRewind();
        await _next(context);
    }
}

Startup.cs

(placez-le au début de la méthode Configure)

app.UseMiddleware<EnableRequestRewindMiddleware>();

Un autre middleware

Cela fait partie du middleware qui nécessite de décompresser les informations POSTed pour vérifier des éléments.

using (var stream = new MemoryStream())
{
    // make sure that body is read from the beginning
    context.Request.Body.Seek(0, SeekOrigin.Begin);
    context.Request.Body.CopyTo(stream);
    string requestBody = Encoding.UTF8.GetString(stream.ToArray());

    // this is required, otherwise model binding will return null
    context.Request.Body.Seek(0, SeekOrigin.Begin);
}
0
Alexei