net crear como asp c# asp.net async-await c#-5.0 executioncontext

crear - web api json c#



Al utilizar la API web de ASP.NET, mi ExecutionContext no fluye en acciones asíncronas (1)

Estoy teniendo dificultades para entender la mecánica detrás de ExecutionContext.

Por lo que he leído en línea, los elementos sensibles al contexto, como la seguridad (Thread Principal), la cultura, etc., deberían fluir a través de hilos asíncronos dentro de los límites de una unidad de trabajo de ejecución.

Aunque me encuentro con errores muy confusos y potencialmente peligrosos. Estoy notando que el CurrentPrincipal de mi hilo se está perdiendo en la ejecución asíncrona.

Aquí hay un ejemplo de ejemplo de la API web de ASP.NET:

Primero, configuremos una configuración simple de la API web con dos manejadores delegados para propósitos de prueba.

Todo lo que hacen es escribir información de depuración y pasar la solicitud / respuesta, excepto el primer "DummyHandler" que establece el principal del hilo, así como una parte de los datos que se compartirán en el contexto (el ID de correlación de la solicitud).

public static class WebApiConfig { public static void Register(HttpConfiguration config) { config.MessageHandlers.Add(new DummyHandler()); config.MessageHandlers.Add(new AnotherDummyHandler()); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); } } public class DummyHandler : DelegatingHandler { protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { CallContext.LogicalSetData("rcid", request.GetCorrelationId()); Thread.CurrentPrincipal = new ClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new[]{ new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "dgdev") }, "myauthisthebest"))); Debug.WriteLine("Dummy Handler Thread: {0}", Thread.CurrentThread.ManagedThreadId); Debug.WriteLine("User: {0}", (Object)Thread.CurrentPrincipal.Identity.Name); Debug.WriteLine("RCID: {0}", CallContext.LogicalGetData("rcid")); return base.SendAsync(request, cancellationToken) .ContinueWith(task => { Debug.WriteLine("Dummy Handler Thread: {0}", Thread.CurrentThread.ManagedThreadId); Debug.WriteLine("User: {0}", (Object)Thread.CurrentPrincipal.Identity.Name); Debug.WriteLine("RCID: {0}", CallContext.LogicalGetData("rcid")); return task.Result; }); } } public class AnotherDummyHandler : MessageProcessingHandler { protected override HttpRequestMessage ProcessRequest(HttpRequestMessage request, CancellationToken cancellationToken) { Debug.WriteLine(" Another Dummy Handler Thread: {0}", Thread.CurrentThread.ManagedThreadId); Debug.WriteLine(" User: {0}", (Object)Thread.CurrentPrincipal.Identity.Name); Debug.WriteLine(" RCID: {0}", CallContext.LogicalGetData("rcid")); return request; } protected override HttpResponseMessage ProcessResponse(HttpResponseMessage response, CancellationToken cancellationToken) { Debug.WriteLine(" Another Dummy Handler Thread: {0}", Thread.CurrentThread.ManagedThreadId); Debug.WriteLine(" User: {0}", (Object)Thread.CurrentPrincipal.Identity.Name); Debug.WriteLine(" RCID: {0}", CallContext.LogicalGetData("rcid")); return response; } }

Suficientemente simple. A continuación, agreguemos un solo ApiController para manejar un HTTP POST, como si estuviera cargando archivos.

public class UploadController : ApiController { public async Task<HttpResponseMessage> PostFile() { Debug.WriteLine(" Thread: {0}", Thread.CurrentThread.ManagedThreadId); Debug.WriteLine(" User: {0}", (Object)Thread.CurrentPrincipal.Identity.Name); Debug.WriteLine(" RCID: {0}", CallContext.LogicalGetData("rcid")); if (!Request.Content.IsMimeMultipartContent()) { throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType); } try { await Request.Content.ReadAsMultipartAsync( new MultipartFormDataStreamProvider( HttpRuntime.AppDomainAppPath + @"upload/temp")); Debug.WriteLine(" Thread: {0}", Thread.CurrentThread.ManagedThreadId); Debug.WriteLine(" User: {0}", (Object)Thread.CurrentPrincipal.Identity.Name); Debug.WriteLine(" RCID: {0}", CallContext.LogicalGetData("rcid")); return new HttpResponseMessage(HttpStatusCode.Created); } catch (Exception e) { return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, e); } } }

Al ejecutar una prueba con Fiddler, esta es la salida que recibo:

Dummy Handler Thread: 63 User: dgdev RCID: 6d542847-4ceb-4511-85e5-d1b5bf3be476 Another Dummy Handler Thread: 63 User: dgdev RCID: 6d542847-4ceb-4511-85e5-d1b5bf3be476 Thread: 63 User: dgdev RCID: 6d542847-4ceb-4511-85e5-d1b5bf3be476 Thread: 77 User: <<< PRINCIPAL IS LOST AFTER ASYNC RCID: 6d542847-4ceb-4511-85e5-d1b5bf3be476 Another Dummy Handler Thread: 63 User: <<< PRINCIPAL IS STILL LOST RCID: 6d542847-4ceb-4511-85e5-d1b5bf3be476 Dummy Handler Thread: 65 User: dgdev <<< PRINCIPAL IS BACK?!? RCID: 6d542847-4ceb-4511-85e5-d1b5bf3be476

Para hacer las cosas más confusas, cuando añado el seguimiento a la línea asíncrona:

await Request.Content.ReadAsMultipartAsync( new MultipartFormDataStreamProvider(..same as before..)) .ConfigureAwait(false); <<<<<<

Ahora recibo esta salida:

Dummy Handler Thread: 40 User: dgdev RCID: 8d944500-cb52-4362-8537-dab405fa12a2 Another Dummy Handler Thread: 40 User: dgdev RCID: 8d944500-cb52-4362-8537-dab405fa12a2 Thread: 40 User: dgdev RCID: 8d944500-cb52-4362-8537-dab405fa12a2 Thread: 65 User: dgdev <<< PRINCIPAL IS HERE! RCID: 8d944500-cb52-4362-8537-dab405fa12a2 Another Dummy Handler Thread: 65 User: <<< PRINCIPAL IS LOST RCID: 8d944500-cb52-4362-8537-dab405fa12a2 Dummy Handler Thread: 40 User: dgdev RCID: 8d944500-cb52-4362-8537-dab405fa12a2

El punto aquí es este. El código que sigue al async my, de hecho, llama a mi lógica empresarial o simplemente requiere que el contexto de seguridad esté configurado correctamente. Hay un problema potencial de integridad en marcha.

¿Alguien puede ayudarnos a arrojar algo de luz qué está pasando?

Gracias por adelantado.


No tengo todas las respuestas, pero puedo ayudar a completar algunos espacios en blanco y adivinar el problema.

Por defecto, el SynchronizationContext ASP.NET fluirá, pero la forma en que fluye la identidad es un poco rara . En realidad fluye HttpContext.Current.User y luego establece Thread.CurrentPrincipal a eso. Así que si solo configura Thread.CurrentPrincipal , no verá que fluya correctamente.

De hecho, verás el siguiente comportamiento:

  • Desde el momento en que Thread.CurrentPrincipal se establece en un subproceso, ese subproceso tendrá el mismo principio hasta que vuelva a entrar en un contexto ASP.NET.
  • Cuando cualquier subproceso entra en el contexto ASP.NET, Thread.CurrentPrincipal se borra (porque se establece en HttpContext.Current.User ).
  • Cuando se utiliza un subproceso fuera del contexto ASP.NET, simplemente retiene cualquier Thread.CurrentPrincipal que se haya establecido en él.

Aplicando esto a su código y salida originales:

  • Los 3 primeros se informan de forma sincrónica desde el subproceso 63 después de que se estableció explícitamente su CurrentPrincipal , por lo que todos tienen el valor esperado.
  • El subproceso 77 se usa para reanudar el método async , por lo que ingresa al contexto ASP.NET y CurrentPrincipal cualquier CurrentPrincipal que haya tenido.
  • El hilo 63 se utiliza para ProcessResponse . Vuelve a entrar en el contexto de ASP.NET, borrando su Thread.CurrentPrincipal .
  • El hilo 65 es el interesante. Se está ejecutando fuera del contexto ASP.NET (en un ContinueWith sin un programador), por lo que solo retiene cualquier CurrentPrincipal que haya tenido antes. Supongo que su CurrentPrincipal solo queda de una ejecución de prueba anterior.

El código actualizado cambia PostFile para ejecutar su segunda parte fuera del contexto ASP.NET. Por lo tanto, recoge el subproceso 65, que por casualidad tiene configurado CurrentPrincipal . Dado que está fuera del contexto de ASP.NET, CurrentPrincipal no se borra.

Entonces, me parece que ExecutionContext está fluyendo bien. Estoy seguro de que Microsoft ha probado que ExecutionContext fluye a través del wazoo; De lo contrario, todas las aplicaciones ASP.NET del mundo tendrían un grave defecto de seguridad. Es importante tener en cuenta que en este código, Thread.CurrentPrincipal solo se refiere a las reclamaciones del usuario actual y no representa la suplantación real.

Si mis suposiciones son correctas, entonces la solución es bastante simple: en SendAsync , cambie esta línea:

Thread.CurrentPrincipal = new ClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new[]{ new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "dgdev") }, "myauthisthebest")));

a esto:

HttpContext.Current.User = new ClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new[]{ new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "dgdev") }, "myauthisthebest"))); Thread.CurrentPrincipal = HttpContext.Current.User;