Besoin de connecter le corps de la requête et de la réponse asp.net webapi 2 à une base de données

103

J'utilise Microsoft Asp.net WebApi2 hébergé sur IIS. Je voudrais très simplement enregistrer le corps de la requête (XML ou JSON) et le corps de la réponse pour chaque message.

Il n'y a rien de spécial à propos de ce projet ou du contrôleur qui traite la publication. Je ne suis pas intéressé par l'utilisation de cadres de journalisation tels que nLog, elmah, log4net ou les fonctionnalités de traçage intégrées de l'API Web, sauf si cela est nécessaire.

Je veux simplement savoir où mettre mon code de journalisation et comment obtenir le JSON ou XML réel à partir de la demande et de la réponse entrantes et sortantes.

Ma méthode de publication de contrôleur:

public HttpResponseMessage Post([FromBody])Employee employee)
{
   if (ModelState.IsValid)
   {
      // insert employee into to the database
   }

}
utilisateur2315985
la source
Cherchez-vous à enregistrer une demande / réponse pour une action particulière, un ensemble ou toutes vos actions dans un contrôleur particulier?
LB2
Uniquement intéressé par la journalisation Post. (a) Heure de publication (b) corps de la réponse xml ou json publiée (c) (le contenu xml ou json) avec le code d'état
HTTP
La raison pour laquelle je demandais est de suggérer s'il faut mettre le code directement en action, ou une solution générique à toutes les actions. Voir ma réponse ci-dessous.
LB2
Pour info, j'ai supprimé asp.net car il n'a pas d'incidence sur cette question
Dalorzo
la création d'un fichier n'est-elle pas une option?
Prerak K

Réponses:

194

Je recommanderais d'utiliser un fichier DelegatingHandler. Ensuite, vous n'aurez pas à vous soucier du code de journalisation dans vos contrôleurs.

public class LogRequestAndResponseHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (request.Content != null)
        {
            // log request body
            string requestBody = await request.Content.ReadAsStringAsync();
            Trace.WriteLine(requestBody);
        }
        // let other handlers process the request
        var result = await base.SendAsync(request, cancellationToken);

        if (result.Content != null)
        {
            // once response body is ready, log it
            var responseBody = await result.Content.ReadAsStringAsync();
            Trace.WriteLine(responseBody);
        }

        return result;
    }
}

Remplacez simplement Trace.WriteLinepar votre code de journalisation et enregistrez le gestionnaire WebApiConfigcomme ceci:

config.MessageHandlers.Add(new LogRequestAndResponseHandler());

Voici la documentation Microsoft complète pour les gestionnaires de messages .

SoftwareFactor
la source
3
task.Result.Contentrevient System.Net.Http.ObjectContent. Existe-t-il un moyen d'obtenir le xml / json brut à la place?
PC.
4
@SoftwareFactor: ContinueWithet Resultsont des API dangereuses. Il serait de loin préférable d'utiliser à la awaitplace, c'est-à-direvar result = await base.SendAsync(request, cancellationToken); var resposeBody = await response.Content.ReadAsStringAsync(); Trace.WriteLine(responseBody); return response;
Stephen Cleary
9
C'est une solution très cool, mais elle générera une erreur lorsque la réponse ne contient aucun corps. Mais c'est assez facile à vérifier et à réparer :)
buddybubble
6
L'appel to await request.Content.ReadAsStringAsync();not entraîne-t-il une erreur indiquant que le flux de demande a déjà été lu dans certaines circonstances?
Gavin
6
Si le gestionnaire de délégation lit le corps de la requête, ne la rendrait-elle pas indisponible pour le gestionnaire de terminal réel (c'est-à-dire mvc / webapi)?
LB2
15

Il existe plusieurs approches pour gérer de manière générique la journalisation des demandes / réponses pour chaque appel de méthode WebAPI:

  1. ActionFilterAttribute: On peut écrire sur mesure ActionFilterAttributeet décorer les méthodes contrôleur / action pour activer la journalisation.

    Vous devez décorer chaque contrôleur / méthode (vous pouvez toujours le faire sur le contrôleur de base, mais cela ne résout toujours pas les problèmes transversaux.

  2. Remplacez BaseControlleret gérez la journalisation là-bas.

    Inconvénient: Nous attendons / forçons les contrôleurs à hériter d'un contrôleur de base personnalisé.

  3. Utilisation DelegatingHandler.

    Avantage: Nous ne touchons pas ici au contrôleur / méthode avec cette approche. Le gestionnaire de délégation est isolé et gère correctement la journalisation des demandes / réponses.

Pour un article plus détaillé, reportez-vous à http://weblogs.asp.net/fredriknormen/log-message-request-and-response-in-asp-net-webapi .

Venkatesh Muniyandi
la source
Vous pouvez attribuer n'importe quel filtre d'action comme suit: classe statique publique WebApiConfig {registre public static void (HttpConfiguration config) {// configuration de l'API Web et services config.Filters.Add (new MyFilter ()) // routes de l'API Web config.MapHttpAttributeRoutes (); config.Routes.MapHttpRoute (nom: "DefaultApi", routeTemplate: "api / {controller} / {id}", par défaut: new {id = RouteParameter.Optional}); }}
Mika Karjunen
11

L'une des options que vous avez consiste à créer un filtre d'action et à décorer votre WebApiController / ApiMethod avec.

Attribut de filtre

public class MyFilterAttribute : System.Web.Http.Filters.ActionFilterAttribute
    {
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            if (actionContext.Request.Method == HttpMethod.Post)
            {
                var postData = actionContext.ActionArguments;
                //do logging here
            }
        }
    }

Contrôleur WebApi

[MyFilterAttribute]
public class ValuesController : ApiController{..}

ou

[MyFilterAttribute]
public void Post([FromBody]string value){..}

J'espère que cela t'aides.

Prerak K
la source
J'aime cette approche mais pour obtenir la réponse, je dois remplacer OnActionExecuted à la place. Le problème est que la demande à ce stade a déjà été convertie en mon POCO au lieu d'être le xml ou json. Des pensées?
user2315985
À l'origine, je voulais dire, enregistrez les données dans OnActionExecuting, puis laissez simplement le message faire son travail. Ce que j'ai compris de votre question, c'est que vous voulez simplement enregistrer les données pour chaque message qui a été fait.
Prerak K
3
Je souhaite enregistrer à la fois la demande et les données de réponse chaque fois que quelqu'un publie.
user2315985
2
vous pouvez utiliser OnActionExecuted et essayer "(actionExecutedContext.ActionContext.Response.Content as ObjectContent) .Value.ToString ()" pour obtenir la réponse et la consigner.
Prerak K
Comment obtenir la demande depuis OnActionExecuted?
user2315985
3

L'accès au message de demande est facile. Votre classe de baseApiController contient une .Requestpropriété qui, comme son nom l'indique, contient la requête sous forme analysée. Il vous suffit de l'examiner pour tout ce que vous cherchez à enregistrer et de le transmettre à votre installation de journalisation, quelle qu'elle soit. Ce code, vous pouvez le mettre au début de votre action, si vous devez le faire pour une seule ou une poignée.

Si vous devez le faire sur toutes les actions (toutes signifiant plus qu'une poignée gérable), alors ce que vous pouvez faire est de remplacer la .ExecuteAsyncméthode pour capturer chaque appel d'action pour votre contrôleur.

public override Task<HttpResponseMessage> ExecuteAsync(
    HttpControllerContext controllerContext,
    CancellationToken cancellationToken
)
{
    // Do logging here using controllerContext.Request
    return base.ExecuteAsync(controllerContext, cancellationToken);
}
LB2
la source
Je fais cela, et je ne l'ai pas encore évalué, juste mon intuition me dit que cela peut être très lent?
Marcus
Pourquoi pensez-vous que ce serait lent? ExecuteAsyncest ce qui est appelé par le framework, et l'implémentation de la classe de contrôleur de base est ce qui fait réellement exécuter l'action. Il s'agit simplement d'appeler votre journalisation dans le cadre d'une exécution déjà en cours. La seule pénalité ici est le temps de faire la journalisation réelle.
LB2
Non, je veux dire, «très lent» comme dans la journalisation de chaque demande.
Marcus
2
Eh bien, c'est une question d'exigences, et c'est l'exigence énoncée par OP. C'est une question de volume manipulé par le site, des performances de la fonction de journalisation, etc. C'est au-delà de la publication des OP.
LB2
0

Cela semble être un fil assez ancien mais partageant une autre solution.

Vous pouvez ajouter cette méthode dans votre fichier global.asax qui sera déclenché après la fin de la requête HTTP.

void Application_EndRequest(Object Sender, EventArgs e)
    {
        var request = (Sender as HttpApplication).Request;
        var response = (Sender as HttpApplication).Response;

        if (request.HttpMethod == "POST" || request.HttpMethod == "PUT")
        {


            byte[] bytes = request.BinaryRead(request.TotalBytes);
            string body = Encoding.UTF7.GetString(bytes);
            if (!String.IsNullOrEmpty(body))
            {


                // Do your logic here (Save in DB, Log in IIS etc.)
            }
        }
    }
EMC
la source
0

C'est un sujet très ancien mais j'ai passé beaucoup de temps (recherche sur Internet) à faire ces choses, je vais donc simplement publier ma solution ici.

Concept

  1. Remplacer ExecuteAsync de la méthode APicontroller pour le suivi de la demande entrante, dans ma solution, je crée Base_ApiController en tant que parent des contrôleurs d'API de mon projet.
  2. Utilisez System.Web.Http.Filters.ActionFilterAttribute pour suivre la réponse sortante du contrôleur d'API
  3. *** (Supplémentaire) *** Utilisez System.Web.Http.Filters.ExceptionFilterAttribute pour se connecter lorsqu'une exception se produit.

1. MyController.cs

    [APIExceptionFilter]  // use 3.
    [APIActionFilter]     // use 2.
    public class Base_APIController : ApiController
    {
        public   bool  IsLogInbound
        {
            get
            { return   ConfigurationManager.AppSettings["LogInboundRequest"] =="Y"? true:false ;     }
        }
        /// <summary>
        /// for logging exception
        /// </summary>
        /// <param name="controllerContext"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public override Task<HttpResponseMessage> ExecuteAsync(
         HttpControllerContext controllerContext,
         CancellationToken cancellationToken
         )
        {
            // Do logging here using controllerContext.Request
            // I don't know why calling the code below make content not null Kanit P.
            var content = controllerContext.Request.Content.ReadAsStringAsync().Result.ToString(); // keep request json content
             // Do your own logging!
            if (IsLogInbound)
            {
                try
                {
                    ErrLog.Insert(ErrLog.type.InboundRequest, controllerContext.Request,
                         controllerContext.Request.RequestUri.AbsoluteUri
                         , content);
                }
                catch (Exception e) { }
            }

            // will not log err when go to wrong controller's action (error here but not go to APIExceptionFilter)
            var t = base.ExecuteAsync(controllerContext, cancellationToken);
            if (!t.Result.IsSuccessStatusCode)
            { 
            }
            return t;

        }

2. APIActionFilter.cs

    public class APIActionFilter : System.Web.Http.Filters.ActionFilterAttribute
    {
        public bool LogOutboundRequest
        {
            get
            { return ConfigurationManager.AppSettings["LogInboundRequest"] == "Y" ? true : false; }
        }

        public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
        {
            try {

                var returndata = actionExecutedContext.Response.Content.ReadAsStringAsync().Result.ToString(); 
             //keep Json response content
             // Do your own logging!
                if (LogOutboundRequest)
                {
                    ErrLog.Insert(ErrLog.type.OutboundResponse, actionExecutedContext.Response.Headers,
                       actionExecutedContext.ActionContext.ControllerContext.ControllerDescriptor.ControllerName
                      + "/"
                      + actionExecutedContext.ActionContext.ActionDescriptor.ActionName
                      , returndata );
                }
            } catch (Exception e) {

            }
     

        } 
    }
}

3. APIExceptionFilter.cs

    public class APIExceptionFilter : ExceptionFilterAttribute
    {
    public bool IsLogErr
    {
        get
        { return ConfigurationManager.AppSettings["LogExceptionRequest"] == "Y" ? true : false; }
    }


    public override void OnException(HttpActionExecutedContext context)
    {
        try
        { 
            //Do your own logging!
            if (IsLogErr)
            {
                ErrLog.Insert(ErrLog.type.APIFilterException, context.Request,
                    context.ActionContext.ControllerContext.ControllerDescriptor.ControllerName
                    + "/"
                    + context.ActionContext.ActionDescriptor.ActionName
                    , context.Exception.ToString() + context.Exception.StackTrace);
            }
        }catch(Exception e){

        }

        if (context.Exception is NotImplementedException)
        {
            context.Response = new HttpResponseMessage(HttpStatusCode.NotImplemented);
        }
        else {
            context.Response = new HttpResponseMessage(HttpStatusCode.InternalServerError);

        }
    }
}
user3682728
la source