ContextBoundObject lanza un error remoto después de esperar

Tengo algún código de registro que se escribió para interceptar llamadas de método usando ContextBoundObject s y ContextAttribute. El código se basa en una muestra de Code Project.

Todo esto funcionó bien hasta que comenzamos a usar esta biblioteca con un código que aprovecha async y espera. Ahora obtenemos errores remotos al ejecutar el código. Aquí hay un ejemplo simple que reproduce el problema:

public class OhMyAttribute : ContextAttribute { public OhMyAttribute() : base("OhMy") { } } [OhMy] public class Class1 : ContextBoundObject { private string one = "1"; public async Task Method1() { Console.WriteLine(one); await Task.Delay(50); Console.WriteLine(one); } } 

Cuando invocamos Method1 obtenemos la siguiente RemotingException en la segunda Console.WriteLine :

 Remoting cannot find field 'one' on type 'WindowsFormsApplication1.Class1'. 

¿Hay alguna forma de solucionar este problema utilizando métodos C # integrados o tenemos que buscar una solución alternativa como PostSharp?

Aquí hay una solución más general.

Tiene las siguientes deficiencias:

  • No admite el cambio de SynchronizationContext dentro de ContextBoundObject . Va a throw en ese caso.
  • No admite el caso de usar await cuando SynchronizationContext.Current es nulo y el TaskScheduler.Current no es el TaskScheduler.Default . En este escenario, normalmente await capturar el TaskScheduler y usarlo para publicar el rest del trabajo, pero como esta solución establece el SynchronizationContext el TaskScheduler no se capturará. Así, cuando se detecte esta situación, se throw .
  • No es compatible con el uso de .ConfigureAwait(false) ya que hará que el SynchronizationContext no sea capturado. Lamentablemente, no pude detectar este caso. Sin embargo, si el usuario desea obtener .ConfigureAwait(false) comportamiento similar a .ConfigureAwait(false) para el SynchronizationContext paso a través subyacente, puede usar un observador personalizado (consulte https://stackoverflow.com/a/22417031/495262 ).

Una cosa interesante aquí es que he intentado crear un “paso a través” de SynchronizationContext . Es decir, no quería sobrescribir ningún SynchronizationContext existente, sino más bien mantener su comportamiento y colocar sobre él el comportamiento de hacer el trabajo en el contexto adecuado. Cualquier comentario sobre un mejor enfoque es bienvenido.

  using System; using System.Runtime.Remoting.Activation; using System.Runtime.Remoting.Contexts; using System.Runtime.Remoting.Messaging; using System.Threading; using System.Threading.Tasks; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { var c1 = new Class1(); var t = c1.Method1(); Func f = c1.Method1; f.BeginInvoke(null, null); Console.ReadKey(); } } [MyContext] public class Class1 : ContextBoundObject { private string one = "1"; public async Task Method1() { Console.WriteLine(one); await Task.Delay(50); Console.WriteLine(one); } } sealed class MyContextAttribute : ContextAttribute { public MyContextAttribute() : base("My") { } public override void GetPropertiesForNewContext(IConstructionCallMessage ctorMsg) { if (ctorMsg == null) throw new ArgumentNullException("ctorMsg"); ctorMsg.ContextProperties.Add(new ContributeInstallContextSynchronizationContextMessageSink()); } public override bool IsContextOK(Context ctx, IConstructionCallMessage ctorMsg) { return false; } } sealed class ContributeInstallContextSynchronizationContextMessageSink : IContextProperty, IContributeServerContextSink { public ContributeInstallContextSynchronizationContextMessageSink() { } public IMessageSink GetServerContextSink(IMessageSink nextSink) { return new InstallContextSynchronizationContextMessageSink(nextSink); } public string Name { get { return "ContributeInstallContextSynchronizationContextMessageSink"; } } public bool IsNewContextOK(Context ctx) { return true; } public void Freeze(Context ctx) { } } sealed class InstallContextSynchronizationContextMessageSink : IMessageSink { readonly IMessageSink m_NextSink; public InstallContextSynchronizationContextMessageSink(IMessageSink nextSink) { m_NextSink = nextSink; } public IMessageSink NextSink { get { return m_NextSink; } } public IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink) { var contextSyncContext = new ContextSynchronizationContext(SynchronizationContext.Current); var syncContextReplacer = new SynchronizationContextReplacer(contextSyncContext); DelegateMessageSink.SyncProcessMessageDelegate replySyncDelegate = (n, m) => SyncProcessMessageDelegateForAsyncReply(n, m, syncContextReplacer); var newReplySink = new DelegateMessageSink(replySink, replySyncDelegate, null); return m_NextSink.AsyncProcessMessage(msg, newReplySink); } public IMessage SyncProcessMessage(IMessage msg) { var contextSyncContext = new ContextSynchronizationContext(SynchronizationContext.Current); using (new SynchronizationContextReplacer(contextSyncContext)) { var ret = m_NextSink.SyncProcessMessage(msg); return ret; } } private IMessage SyncProcessMessageDelegateForAsyncReply(IMessageSink nextSink, IMessage msg, SynchronizationContextReplacer syncContextReplacer) { syncContextReplacer.Dispose(); return nextSink.SyncProcessMessage(msg); } private void PreChecks() { if (SynchronizationContext.Current != null) return; if (TaskScheduler.Current != TaskScheduler.Default) throw new InvalidOperationException("InstallContextSynchronizationContextMessageSink does not support calling methods with SynchronizationContext.Current as null while Taskscheduler.Current is not TaskScheduler.Default"); } } sealed class SynchronizationContextReplacer : IDisposable { SynchronizationContext m_original; SynchronizationContext m_new; public SynchronizationContextReplacer(SynchronizationContext syncContext) { m_original = SynchronizationContext.Current; m_new = syncContext; SynchronizationContext.SetSynchronizationContext(m_new); } public void Dispose() { // We don't expect the SynchronizationContext to be changed during the lifetime of the SynchronizationContextReplacer if (SynchronizationContext.Current != m_new) throw new InvalidOperationException("SynchronizationContext was changed unexpectedly."); SynchronizationContext.SetSynchronizationContext(m_original); } } sealed class ContextSynchronizationContext : PassThroughSynchronizationConext { readonly Context m_context; private ContextSynchronizationContext(SynchronizationContext passThroughSyncContext, Context ctx) : base(passThroughSyncContext) { if (ctx == null) throw new ArgumentNullException("ctx"); m_context = ctx; } public ContextSynchronizationContext(SynchronizationContext passThroughSyncContext) : this(passThroughSyncContext, Thread.CurrentContext) { } protected override SynchronizationContext CreateCopy(SynchronizationContext copiedPassThroughSyncContext) { return new ContextSynchronizationContext(copiedPassThroughSyncContext, m_context); } protected override void CreateSendOrPostCallback(SendOrPostCallback d, object state) { CrossContextDelegate ccd = () => d(state); m_context.DoCallBack(ccd); } } abstract class PassThroughSynchronizationConext : SynchronizationContext { readonly SynchronizationContext m_passThroughSyncContext; protected PassThroughSynchronizationConext(SynchronizationContext passThroughSyncContext) : base() { m_passThroughSyncContext = passThroughSyncContext; } protected abstract void CreateSendOrPostCallback(SendOrPostCallback d, object state); protected abstract SynchronizationContext CreateCopy(SynchronizationContext copiedPassThroughSyncContext); public sealed override void Post(SendOrPostCallback d, object state) { var d2 = CreateSendOrPostCallback(d); if (m_passThroughSyncContext != null) m_passThroughSyncContext.Post(d2, state); else base.Post(d2, state); } public sealed override void Send(SendOrPostCallback d, object state) { var d2 = CreateSendOrPostCallback(d); if (m_passThroughSyncContext != null) m_passThroughSyncContext.Send(d2, state); else base.Send(d2, state); } public sealed override SynchronizationContext CreateCopy() { var copiedSyncCtx = m_passThroughSyncContext != null ? m_passThroughSyncContext.CreateCopy() : null; return CreateCopy(copiedSyncCtx); } public sealed override void OperationCompleted() { if (m_passThroughSyncContext != null) m_passThroughSyncContext.OperationCompleted(); else base.OperationCompleted(); } public sealed override void OperationStarted() { if (m_passThroughSyncContext != null) m_passThroughSyncContext.OperationStarted(); else base.OperationStarted(); } public sealed override int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout) { return m_passThroughSyncContext != null ? m_passThroughSyncContext.Wait(waitHandles, waitAll, millisecondsTimeout) : base.Wait(waitHandles, waitAll, millisecondsTimeout); } private SendOrPostCallback CreateSendOrPostCallback(SendOrPostCallback d) { SendOrPostCallback sopc = s => CreateSendOrPostCallback(d, s); return sopc; } } sealed class DelegateMessageSink : IMessageSink { public delegate IMessage SyncProcessMessageDelegate(IMessageSink nextSink, IMessage msg); public delegate IMessageCtrl AsyncProcessMessageDelegate(IMessageSink nextSink, IMessage msg, IMessageSink replySink); readonly IMessageSink m_NextSink; readonly SyncProcessMessageDelegate m_syncProcessMessageDelegate; readonly AsyncProcessMessageDelegate m_asyncProcessMessageDelegate; public DelegateMessageSink(IMessageSink nextSink, SyncProcessMessageDelegate syncProcessMessageDelegate, AsyncProcessMessageDelegate asyncProcessMessageDelegate) { m_NextSink = nextSink; m_syncProcessMessageDelegate = syncProcessMessageDelegate; m_asyncProcessMessageDelegate = asyncProcessMessageDelegate; } public IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink) { return (m_asyncProcessMessageDelegate != null) ? m_asyncProcessMessageDelegate(m_NextSink, msg, replySink) : m_NextSink.AsyncProcessMessage(msg, replySink); } public IMessageSink NextSink { get { return m_NextSink; } } public IMessage SyncProcessMessage(IMessage msg) { return (m_syncProcessMessageDelegate != null) ? m_syncProcessMessageDelegate(m_NextSink, msg) : m_NextSink.SyncProcessMessage(msg); } } } 

Respuesta corta: las llamadas remotas no funcionan en campos privados. La reescritura de async / await provoca un bash de realizar una llamada remota en un campo privado.

El problema se puede reproducir sin async / await . Y demostrarlo de esta manera es útil para comprender lo que está sucediendo en el caso de async / await :

 [OhMy] public class Class2 : ContextBoundObject { private string one = "1"; public void Method1() { var nc = new NestedClass(this); } public class NestedClass { public NestedClass(Class2 c2) { Console.WriteLine(c2.one); // Note: nested classes are allowed access to outer classes privates } } } static void Main(string[] args) { var c2 = new Class2(); // This call causes no problems: c2.Method1(); // This, however, causes the issue. var nc = new Class2.NestedClass(c2); } 

Vayamos a través de lo que sucede línea por línea:

  1. En Main, comenzamos en Context0
  2. Dado que Class2 es un objeto ContextBoundObject y como OhMyAttribute considera que el contexto actual es inaceptable, se crea una instancia de Class2 en Context1 (llamaré a esto c2_real , y lo que se devuelve y almacena en c2 es un proxy c2_real para c2_real .
  3. Cuando se llama a c2.Method1() , se llama en el proxy remoto. Dado que estamos en Context0, el proxy remoto se da cuenta de que no está en el contexto correcto, por lo que cambia a Context1, y se ejecuta el código dentro de Method1 . 3.a Dentro del NestedClass , llamamos al constructor NestedClass que usa c2.one . En este caso, ya estamos en Context1, por lo que c2.one no requiere cambios de contexto y, por lo tanto, estamos utilizando el objeto c2_real directamente.

Ahora, el caso problemático:

  1. Creamos una nueva NestedClass pasa en el proxy remoto c2 . No se producen cambios de contexto aquí porque NestedClass no es un objeto ContextBoundObject .
  2. Dentro del ctor NestedClass , accede a c2.one. El proxy remoto se da cuenta de que aún estamos en Context0 y, por lo tanto, intenta remover esta llamada a Context1. Esto falla porque c2.one es un campo privado. Verás en Object.GetFieldInfo que solo busca campos públicos:

     private FieldInfo GetFieldInfo(String typeName, String fieldName) { // ... FieldInfo fldInfo = t.GetField(fieldName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); if(null == fldInfo) { #if FEATURE_REMOTING throw new RemotingException(String.Format( CultureInfo.CurrentCulture, Environment.GetResourceString("Remoting_BadField"), fieldName, typeName)); // ... } return fldInfo; } 

Entonces, ¿cómo async / await termina causando este mismo problema?

El async / await hace que su Class1 se reescriba de manera que use una clase anidada con una máquina de estado (usó ILSpy para generar):

 public class Class1 : ContextBoundObject { // ... private struct d__0 : IAsyncStateMachine { public int <>1__state; public AsyncTaskMethodBuilder <>t__builder; public Class1 <>4__this; private TaskAwaiter <>u__$awaiter1; private object <>t__stack; void IAsyncStateMachine.MoveNext() { try { int num = this.<>1__state; if (num != -3) { TaskAwaiter taskAwaiter; if (num != 0) { Console.WriteLine(this.<>4__this.one); taskAwaiter = Task.Delay(50).GetAwaiter(); if (!taskAwaiter.IsCompleted) { this.<>1__state = 0; this.<>u__$awaiter1 = taskAwaiter; this.<>t__builder.AwaitUnsafeOnCompletedd__0>(ref taskAwaiter, ref this); return; } } else { taskAwaiter = this.<>u__$awaiter1; this.<>u__$awaiter1 = default(TaskAwaiter); this.<>1__state = -1; } taskAwaiter.GetResult(); taskAwaiter = default(TaskAwaiter); Console.WriteLine(this.<>4__this.one); } } catch (Exception exception) { this.<>1__state = -2; this.<>t__builder.SetException(exception); return; } this.<>1__state = -2; this.<>t__builder.SetResult(); } // ... } private string one = "1"; public Task Method1() { Class1.d__0 d__; d__.<>4__this = this; d__.<>t__builder = AsyncTaskMethodBuilder.Create(); d__.<>1__state = -1; AsyncTaskMethodBuilder <>t__builder = d__.<>t__builder; <>t__builder.Startd__0>(ref d__); return d__.<>t__builder.Task; } } 

Lo importante a notar es que

  • Se crea una estructura anidada que tiene acceso a los datos privados de Class1
  • La variable this se levanta y se almacena en la clase anidada.

Entonces, lo que pasa aquí es que

  1. En la llamada inicial a c1.Method1() los avisos del proxy remoto están en Context0, y eso necesita cambiar a Context1.
  2. Finalmente, se llama MoveNext se llama a c1.one . Como ya estamos en Context1, no es necesario un cambio de contexto (por lo que el problema no se produce).
  3. Posteriormente, desde que se registró una continuación, se realizará una llamada a MoveNext para ejecutar el rest del código después de la await . Sin embargo, esta llamada a MoveNext no ocurrirá dentro de una llamada a uno de los métodos de Class1 . Por lo tanto, cuando el código c1.one se ejecute esta vez, estaremos en Context0. El proxy remoto se encuentra en Context0 e intenta un cambio de contexto. Esto provoca el mismo error que el anterior, ya que c1.one es un campo privado.

Solución: no estoy seguro de una solución general, pero para este caso específico puede solucionar el problema al no usar this referencia en el método. Es decir:

 public async Task Method1() { var temp = one; Console.WriteLine(temp); await Task.Delay(50); Console.WriteLine(temp); } 

O cambie a usar una propiedad privada en lugar de un campo.