Necesidad de registrar el cuerpo de solicitud y respuesta de asp.net webapi 2 en una base de datos

103

Estoy usando Microsoft Asp.net WebApi2 alojado en IIS. Simplemente me gustaría registrar el cuerpo de la solicitud (XML o JSON) y el cuerpo de la respuesta para cada publicación.

No hay nada especial en este proyecto o en el controlador que procesa la publicación. No estoy interesado en utilizar marcos de registro como nLog, elmah, log4net o las funciones de seguimiento integradas de la API web, a menos que sea necesario.

Simplemente quiero saber dónde poner mi código de registro y cómo obtener el JSON o XML real de la solicitud y respuesta entrante y saliente.

Mi método de publicación del controlador:

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

}
usuario2315985
fuente
¿Está buscando registrar Solicitud / Respuesta para una acción en particular, un conjunto o todas sus acciones en un controlador en particular?
LB2
Solo interesado en registrar Post. (a) Hora de publicación (b) cuerpo de xml o json publicado (c) respuesta (el contenido xml o json) junto con el código de estado Http
user2315985
La razón por la que preguntaba es para sugerir si poner el código directamente en acción o una solución genérica para todas las acciones. Vea mi respuesta a continuación.
LB2
Para su información
eliminé
¿Crear un declarante no es una opción?
Prerak K

Respuestas:

194

Recomendaría usar un DelegatingHandler. Entonces no tendrá que preocuparse por ningún código de registro en sus controladores.

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;
    }
}

Simplemente reemplácelo Trace.WriteLinecon su código de inicio de sesión y registre el controlador de WebApiConfigesta manera:

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

Aquí está la documentación completa de Microsoft para los controladores de mensajes .

SoftwareFactor
fuente
3
task.Result.Contentdevuelve System.Net.Http.ObjectContent. ¿Hay alguna forma de obtener el xml / json sin formato en su lugar?
PC.
4
@SoftwareFactor: ContinueWithy Resultson API peligrosas. Sería mucho mejor usar awaiten su lugar, es decir,var result = await base.SendAsync(request, cancellationToken); var resposeBody = await response.Content.ReadAsStringAsync(); Trace.WriteLine(responseBody); return response;
Stephen Cleary
9
Esta es una solución muy buena, sin embargo, arrojará un error cuando la respuesta no contenga cuerpo. Pero eso es bastante fácil de verificar y arreglar :)
buddybubble
6
¿La llamada await request.Content.ReadAsStringAsync();no da como resultado un error que indica que el flujo de solicitud ya se ha leído en determinadas circunstancias?
Gavin
6
Si el controlador delegado lee el cuerpo de la solicitud, ¿no dejaría de estar disponible para el controlador de terminal real (es decir, mvc / webapi)?
LB2
15

Hay varios enfoques para manejar genéricamente el registro de solicitudes / respuestas para cada llamada al método WebAPI:

  1. ActionFilterAttribute: Uno puede escribir ActionFilterAttributemétodos personalizados y decorar el controlador / acción para habilitar el registro.

    Desventaja: debe decorar todos los controladores / métodos (aún puede hacerlo en el controlador base, pero aún así no aborda las preocupaciones transversales.

  2. Anule BaseControllery controle el registro allí.

    Con: Estamos esperando / forzando a los controladores a heredar de un controlador base personalizado.

  3. Usando DelegatingHandler.

    Ventaja: No estamos tocando el controlador / método aquí con este enfoque. El controlador delegador se encuentra aislado y maneja con gracia el registro de solicitudes / respuestas.

Para obtener un artículo más detallado, consulte este http://weblogs.asp.net/fredriknormen/log-message-request-and-response-in-asp-net-webapi .

Venkatesh Muniyandi
fuente
Puede asignar cualquier filtro de acción de la siguiente manera: public static class WebApiConfig {public static void Register (HttpConfiguration config) {// Configuración de API web y servicios config.Filters.Add (new MyFilter ()) // Rutas de API web config.MapHttpAttributeRoutes (); config.Routes.MapHttpRoute (nombre: "DefaultApi", routeTemplate: "api / {controller} / {id}", defaults: new {id = RouteParameter.Optional}); }}
Mika Karjunen
11

Una de las opciones que tiene es crear un filtro de acción y decorar su WebApiController / ApiMethod con él.

Atributo de filtro

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
            }
        }
    }

Controlador WebApi

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

o

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

Espero que esto ayude.

Prerak K
fuente
Me gusta este enfoque, pero para obtener la respuesta tengo que anular OnActionExecuted en su lugar. El problema es que la solicitud en ese momento ya se ha convertido a mi POCO en lugar de ser xml o json. ¿Alguna idea?
user2315985
Originalmente quise decir, registrar datos en OnActionExecuting y luego dejar que la publicación haga su trabajo. Lo que entendí de su pregunta fue que solo desea registrar los datos de cada publicación que se haya realizado.
Prerak K
3
Quiero registrar tanto la solicitud como los datos de respuesta cada vez que alguien publica.
user2315985
2
puede utilizar OnActionExecuted y probar "(actionExecutedContext.ActionContext.Response.Content como ObjectContent) .Value.ToString ()" para obtener la respuesta y registrarla.
Prerak K
¿Cómo obtengo la solicitud desde OnActionExecuted?
user2315985
3

Obtener acceso al mensaje de solicitud es fácil. Su clase baseApiController contiene la .Requestpropiedad que, como su nombre indica, contiene la solicitud en forma analizada. Simplemente examínelo para lo que sea que esté buscando para registrar y páselo a su instalación de registro, cualquiera que sea. Este código puede ponerlo al comienzo de su acción, si necesita hacerlo solo para uno o para un puñado.

Si necesita hacerlo en todas las acciones (todo significa más que un puñado manejable), entonces lo que puede hacer es anular el .ExecuteAsyncmétodo para capturar cada llamada de acción para su controlador.

public override Task<HttpResponseMessage> ExecuteAsync(
    HttpControllerContext controllerContext,
    CancellationToken cancellationToken
)
{
    // Do logging here using controllerContext.Request
    return base.ExecuteAsync(controllerContext, cancellationToken);
}
LB2
fuente
Estoy haciendo esto, y aún no lo he comparado, solo mi intuición me dice que esto puede ser muy lento.
Marcus
¿Por qué crees que sería lento? ExecuteAsynces lo que llama el marco, y la implementación de la clase del controlador base es lo que realmente hace que se ejecute la acción. Esto es solo llamar a su registro como parte de una ejecución que ya está ocurriendo. La única penalización aquí es el momento de realizar el registro real.
LB2
No, quiero decir, 'muy lento' como en el registro de cada solicitud.
Marcus
2
Bueno, esa es una cuestión de requisitos, y ese es el requisito establecido por OP. Es una cuestión del volumen que maneja el sitio, el rendimiento de la instalación de registro, etc. Eso está más allá de la publicación de OP.
LB2
0

Este parece ser un hilo bastante antiguo, pero vale la pena compartir otra solución.

Puede agregar este método en su archivo global.asax que se activará cada vez que finalice la solicitud 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
fuente
0

Este es un tema realmente antiguo, pero pasé mucho tiempo (buscando en Internet) para hacer esto, así que solo publicaré mi solución aquí.

Concepto

  1. Anular ExecuteAsync del método APicontroller para rastrear la solicitud entrante, en mi solución creo Base_ApiController como padre de los controladores API de mi proyecto.
  2. Utilice System.Web.Http.Filters.ActionFilterAttribute para rastrear la respuesta de salida del controlador api
  3. *** (Adicional) *** Utilice System.Web.Http.Filters.ExceptionFilterAttribute para registrar cuando ocurra una excepción.

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
fuente