Comment lire le corps de la requête dans un contrôleur webapi principal asp.net?

106

J'essaie de lire le corps de la requête dans la OnActionExecutingméthode, mais j'obtiens toujours nullle 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 n'a pas non plus fonctionné. Puisqu'il s'agit d'ASP.NET CORE, les choses sont un peu différentes, je pense. Je peux voir tous les exemples ici faisant référence aux anciennes versions de webapi.
Y a-t-il une autre façon de faire cela?

Kasun Koswattha
la source
4
Attention, si le corps de la requête a déjà été lu avant lors du pipeline de requêtes, il est vide lorsque vous essayez de le lire une deuxième fois
Fabio
1
Possible duplication de Read HttpContent dans le contrôleur WebApi
Fabio
@Fabio Merci pour l'info, pouvons-nous définir la position et la relire?
Kasun Koswattha
@KasunKoswattha - De par sa conception, le contenu du corps est traité comme un flux avant uniquement qui ne peut être lu qu'une seule fois.
user270576
Je suppose que la question cible plutôt les filtres ou les intergiciels que les contrôleurs.
Jim Aho du

Réponses:

112

Dans ASP.Net Core, il semble compliqué de lire plusieurs fois la requête de corps, mais si votre première tentative le fait correctement, vous devriez être bien pour les prochaines tentatives.

J'ai lu plusieurs retournements par exemple en remplaçant le flux corporel, mais je pense que ce qui suit est le plus propre:

Les points les plus importants étant

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

[ÉDITER]

Comme le souligne Murad, vous pouvez également profiter de l'extension .Net Core 2.1: EnableBufferingelle stocke les requêtes volumineuses sur le disque au lieu de les garder en mémoire, évitant ainsi les problèmes de gros flux stockés en mémoire (fichiers, images, ...) . Vous pouvez modifier le dossier temporaire en définissant ASPNETCORE_TEMPla variable d'environnement et 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 à nouveau utiliser le corps dans le gestionnaire de requêtes.

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

Attention toutefois si vous gérez des flux volumineux, ce comportement implique que tout est chargé en mémoire, cela ne doit 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échargez des fichiers volumineux, cela devrait être désactivé 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>();
    }

}
Jean
la source
le flux est toujours vide même si je reviens en position 0.
Adrian Nasui
2
Avez-vous utilisé req.EnableRewind();? J'utilise le code ci-dessus et cela fonctionne bien.
Jean
ont utilisé req.EnableRewind (); ne marche pas. J'obtiens Position = 0, body length = 26, mais la lecture du flux «body» aboutit à une chaîne vide.
Adrian Nasui
Vous devez activer le rembobinage avant la première lecture du corps et pas avant la deuxième, sinon je pense que cela ne fonctionnera pas
Jean
3
Il est également possible d'utiliser request.EnableBuffering()(wrapper over EnableRewind()) Il est disponible dans ASP.NET Core 2.1 docs.microsoft.com/en-us/dotnet/api/…
Murad
27

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

Classe de filtre

using Microsoft.AspNetCore.Authorization;
// For ASP.NET 2.1
using Microsoft.AspNetCore.Http.Internal;
// For ASP.NET 3.1
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;

public class ReadableBodyStreamAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        // For ASP.NET 2.1
        // context.HttpContext.Request.EnableRewind();
        // For ASP.NET 3.1
        // context.HttpContext.Request.EnableBuffering();
    }
}

Dans un contrôleur

[HttpPost]
[ReadableBodyStream]
public string SomePostMethod()
{
    //Note: if you're late and body has already been read, you may need this next line
    //Note2: if "Note" is true and Body was read using StreamReader too, then it may be necessary to set "leaveOpen: true" for that stream.
    HttpContext.Request.Body.Seek(0, SeekOrigin.Begin);

    using (StreamReader stream = new StreamReader(HttpContext.Request.Body))
    {
        string body = stream.ReadToEnd();
        // body = "param=somevalue&param2=someothervalue"
    }
}
Andriod
la source
2
Pour netcore3.0, ce serait .EnableBuffering () au lieu de.EnableRewind()
mr5
Thanks @ mr5 - Mis à jour ma réponse
Andriod
J'ai trouvé cela en corrigeant certaines mises à niveau de .net Core 2.2 -> Core 3.1 qui cassaient la méthode EnableRewind (). Je pense que cela nécessite une ligne de code supplémentaire, sans laquelle je ne pourrais pas relire le corps: HttpContext.Request.Body.Seek (0, SeekOrigin.Begin);
Larry Smith le
2
Cela n'a fonctionné pour moi qu'après le passage AuthorizeAttributeà Attribute(dans ASP.Net Core 3.1).
Sigmund
Guys pls assurez-vous d'ajouter les bibliothèques mentionnées. J'avais déjà le code mais EnableBuffering montrait une ligne ondulée rouge jusqu'à ce que je réalise que la référence Microsoft.AspNetCore.Http était manquante. Merci à Android!
kaarthick raman le
18

Pour pouvoir rembobiner le corps de la requête, la réponse de @ Jean m'a aidé à trouver une solution qui semble bien fonctionner. J'utilise actuellement ceci pour le middleware Global Exception Handler mais le principe est le même.

J'ai créé un middleware qui permet essentiellement le rembobinage sur le corps de la requête (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>();
    }
}

Cela peut ensuite être utilisé dans votre Startup.cscomme ainsi:

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

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

SaoBiz
la source
1
Cela a très bien fonctionné pour moi @SaoBiz merci! une faute de frappe, remplacez -le par le constructeur en UseEnableRequestRewind(this IApplicationBuilder builder).
Richard Logwood
@RichardLogwood Heureux que cela ait aidé! Merci d'avoir trouvé la faute de frappe! Fixé. :)
SaoBiz
6

C'est un peu un vieux fil de discussion, mais depuis que je suis arrivé ici, je me suis dit que je publierais mes résultats afin qu'ils puissent aider les autres.

Tout d'abord, j'ai eu le même problème, où je voulais obtenir le Request.Body et faire quelque chose avec ça (journalisation / audit). Mais sinon, je voulais que le point final ait le même aspect.

Donc, il semblait que l'appel EnableBuffering () pourrait faire l'affaire. Ensuite, vous pouvez faire une recherche (0, xxx) sur le corps et relire le contenu, etc.

Cependant, cela a conduit à mon prochain numéro. J'obtiendrais des exceptions «Les opérations synchrones sont interdites» lors de l'accès au point final. Donc, la solution de contournement consiste à définir la propriété AllowSynchronousIO = true, dans les options. Il existe un certain nombre de façons d'accomplir cela (mais il n'est pas important de détailler ici.)

ALORS, le problème suivant est que lorsque je vais lire la requête, le corps a déjà été éliminé. Pouah. Alors, qu'est-ce qui donne?

J'utilise Newtonsoft.JSON comme analyseur [FromBody] dans l'appel endpiont. C'est ce qui est responsable des lectures synchrones et cela ferme également le flux une fois terminé. Solution? Lire le flux avant de passer à l'analyse JSON? Bien sûr, cela fonctionne et je me suis retrouvé avec ceci:

 /// <summary>
/// quick and dirty middleware that enables buffering the request body
/// </summary>
/// <remarks>
/// this allows us to re-read the request body's inputstream so that we can capture the original request as is
/// </remarks>
public class ReadRequestBodyIntoItemsAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        if (context == null) return;

        // NEW! enable sync IO beacuse the JSON reader apparently doesn't use async and it throws an exception otherwise
        var syncIOFeature = context.HttpContext.Features.Get<IHttpBodyControlFeature>();
        if (syncIOFeature != null)
        {
            syncIOFeature.AllowSynchronousIO = true;

            var req = context.HttpContext.Request;

            req.EnableBuffering();

            // read the body here as a workarond for the JSON parser disposing the stream
            if (req.Body.CanSeek)
            {
                req.Body.Seek(0, SeekOrigin.Begin);

                // if body (stream) can seek, we can read the body to a string for logging purposes
                using (var reader = new StreamReader(
                     req.Body,
                     encoding: Encoding.UTF8,
                     detectEncodingFromByteOrderMarks: false,
                     bufferSize: 8192,
                     leaveOpen: true))
                {
                    var jsonString = reader.ReadToEnd();

                    // store into the HTTP context Items["request_body"]
                    context.HttpContext.Items.Add("request_body", jsonString);
                }

                // go back to beginning so json reader get's the whole thing
                req.Body.Seek(0, SeekOrigin.Begin);
            }
        }
    }
}

Alors maintenant, je peux accéder au corps en utilisant le HttpContext.Items ["request_body"] dans les points de terminaison qui ont l'attribut [ReadRequestBodyIntoItems].

Mais mec, cela semble être beaucoup trop de cerceaux à franchir. Alors, voici où j'ai terminé, et j'en suis vraiment content.

Mon point de terminaison a commencé comme quelque chose comme:

[HttpPost("")]
[ReadRequestBodyIntoItems]
[Consumes("application/json")]
public async Task<IActionResult> ReceiveSomeData([FromBody] MyJsonObjectType value)
{
    val bodyString = HttpContext.Items["request_body"];
    // use the body, process the stuff...
}

Mais il est beaucoup plus simple de simplement changer la signature, comme ceci:

[HttpPost("")]
[Consumes("application/json")]
public async Task<IActionResult> ReceiveSomeData()
{
    using (var reader = new StreamReader(
           Request.Body,
           encoding: Encoding.UTF8,
           detectEncodingFromByteOrderMarks: false
    ))
    {
        var bodyString = await reader.ReadToEndAsync();

        var value = JsonConvert.DeserializeObject<MyJsonObjectType>(bodyString);

        // use the body, process the stuff...
    }
}

J'ai vraiment aimé cela car il ne lit qu'une seule fois le flux corporel et j'ai le contrôle de la désérialisation. Bien sûr, c'est bien si ASP.NET core fait cette magie pour moi, mais ici je ne perds pas de temps à lire le flux deux fois (peut-être en tamponnant à chaque fois), et le code est assez clair et propre.

Si vous avez besoin de cette fonctionnalité sur de nombreux points de terminaison, peut-être que les approches middleware pourraient être plus propres, ou vous pouvez au moins encapsuler l'extraction de corps dans une fonction d'extension pour rendre le code plus concis.

Quoi qu'il en soit, je n'ai trouvé aucune source qui aborde les 3 aspects de ce problème, d'où cet article. Espérons que cela aide quelqu'un!

BTW: cela utilisait ASP .NET Core 3.1.

Shane Anderson
la source
Si le programme ne peut pas analyser la chaîne JSON en NyObjectType, alors je ne peux pas lire la valeur de "
request_body
2

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 POSTées et effectuer des contrôles de sécurité par rapport à celles-ci
  • 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 à d' SaoBizavoir signalé cette solution.

Donc, la solution évidente est de permettre à la demande d'être rembobinable, mais assurez-vous qu'après 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 POSTées 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);
}
Alexei
la source
2

Récemment, je suis tombé sur une solution très élégante qui prend en JSON aléatoire dont vous n'avez aucune idée de la structure:

    [HttpPost]
    public JsonResult Test([FromBody] JsonElement json)
    {
        return Json(json);
    }

C'est aussi simple que ça.

naspinski
la source
1

La IHttpContextAccessorméthode fonctionne si vous souhaitez emprunter cette voie.

TLDR;

  • Injectez le IHttpContextAccessor

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

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

Plus - Une tentative d'exemple concis et non compilant des éléments dont vous devrez vous assurer qu'ils sont en place afin d'obtenir un fichier utilisable IHttpContextAccessor. Les réponses ont indiqué correctement que vous devrez revenir au début lorsque vous essayez de lire le corps de la requête. Les CanSeek, Positionpropriétés sur le flux corps de la demande utile pour vérifier cela.

Documentation .NET Core DI

// 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");
        }
    }
}    
Randy Larson
la source
1

J'ai pu lire le corps de la requête dans une application asp.net core 3.1 comme celle-ci (avec un middleware simple qui permet la mise en mémoire tampon - le rembobinage activable semble fonctionner pour les versions antérieures de .Net Core-):

var reader = await Request.BodyReader.ReadAsync();
Request.Body.Position = 0;
var buffer = reader.Buffer;
var body = Encoding.UTF8.GetString(buffer.FirstSpan);
Request.Body.Position = 0;
Ruzgarustu
la source
0

pour lire Body, vous pouvez lire de manière asynchrone.

utilisez la asyncméthode comme suit:

public async Task<IActionResult> GetBody()
{
      string body="";
      using (StreamReader stream = new StreamReader(Request.Body))
      {
           body = await stream.ReadToEndAsync();
      }
    return Json(body);
}

Test avec le facteur:

entrez la description de l'image ici

Cela fonctionne bien et testé en Asp.net coreversion 2.0 , 2.1 , 2.2, 3.0.

J'espère que c'est utile.

AminRostami
la source
0

Je voulais également lire le Request.Body sans le mapper automatiquement à un modèle de paramètre d'action. Testé de nombreuses façons différentes avant de résoudre ce problème. Et je n'ai pas trouvé de solution de travail décrite ici. Cette solution est actuellement basée sur le framework .NET Core 3.0.

reader.readToEnd () cousu comme une manière simple, même s'il était compilé, il lançait une exception d'exécution qui m'obligeait à utiliser un appel asynchrone. Donc, à la place, j'ai utilisé ReadToEndAsync (), mais cela fonctionnait parfois, et parfois non. Me donnant des erreurs comme, impossible de lire après la fermeture du flux. Le problème est que nous ne pouvons pas garantir qu'il retournera le résultat dans le même thread (même si nous utilisons l'attente). Nous avons donc besoin d'une sorte de rappel. Cette solution a fonctionné pour moi.

[Route("[controller]/[action]")]
public class MyController : ControllerBase
{

    // ...

    [HttpPost]
    public async void TheAction()
    {
        try
        {
            HttpContext.Request.EnableBuffering();
            Request.Body.Position = 0;
            using (StreamReader stream = new StreamReader(HttpContext.Request.Body))
            {
                var task = stream
                    .ReadToEndAsync()
                    .ContinueWith(t => {
                        var res = t.Result;
                        // TODO: Handle the post result!
                    });

                // await processing of the result
                task.Wait();
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to handle post!");
        }
    }
krazor
la source
0

La manière la plus simple de procéder est la suivante:

  1. Dans la méthode Controller dont vous devez extraire le corps, ajoutez ce paramètre: [FromBody] SomeClass value

  2. Déclarez "SomeClass" comme: class SomeClass {chaîne publique SomeParameter {get; ensemble; }}

Lorsque le corps brut est envoyé en json, .net core sait le lire très facilement.

abelabbesnabi
la source
0

À ceux qui veulent simplement obtenir le contenu (corps de la requête) de la requête:

Utilisez l' [FromBody]attribut dans le paramètre de méthode de votre contrôleur.

[Route("api/mytest")]
[ApiController]
public class MyTestController : Controller
{
    [HttpPost]
    [Route("content")]
    public async Task<string> ReceiveContent([FromBody] string content)
    {
        // Do work with content
    }
}

Comme le dit doc: cet attribut spécifie qu'un paramètre ou une propriété doit être lié à l'aide du corps de la requête.

sirazal
la source
0

Un moyen rapide d'ajouter la mise en mémoire tampon des réponses dans .NET Core 3.1 est

    app.Use((context, next) =>
    {
        context.Request.EnableBuffering();
        return next();
    });

dans Startup.cs. J'ai trouvé que cela garantit également que la mise en mémoire tampon sera activée avant la lecture du flux, ce qui était un problème pour .Net Core 3.1 avec certaines des autres réponses de filtre middleware / d'autorisation que j'ai vues.

Ensuite, vous pouvez lire le corps de votre requête via HttpContext.Request.Bodydans votre gestionnaire comme plusieurs autres l'ont suggéré.

Il convient également de prendre en EnableBufferingcompte les surcharges qui vous permettent de limiter la quantité de mémoire tampon en mémoire avant d'utiliser un fichier temporaire, ainsi qu'une limite globale de votre mémoire tampon. NB si une requête dépasse cette limite une exception sera levée et la requête n'atteindra jamais votre gestionnaire.

Stephen Wilkinson
la source
Cela a très bien fonctionné pour moi (3.1). Vous a cité une autre question: stackoverflow.com/a/63525694/6210068
Lance