ContextBoundObject在等待之后引发远程处理错误

Joh*_*ner 3 c# custom-attributes async-await

我有一些日志代码,使用ContextBoundObject和ContextAttribute编写拦截方法调用.该代码基于代码项目示例.

这一切都运行良好,直到我们开始使用此库与利用异步和等待的代码.现在我们在运行代码时遇到了远程错误.这是一个重现问题的简单示例:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

当我们调用时,Method1我们RemotingException在第二个上面得到以下内容Console.WriteLine:

Remoting cannot find field 'one' on type 'WindowsFormsApplication1.Class1'.
Run Code Online (Sandbox Code Playgroud)

有没有办法使用内置的C#方法来解决这个问题,还是我们必须看看像PostSharp这样的替代解决方案?

Mat*_*ith 7

简短回答:远程呼叫不适用于私人领域.该async/ await重写导致企图使在私人领域中的远程调用.

没有async/ 可以复制这个问题await.以这种方式展示它有助于理解async/ awaitcase 中发生的事情:

[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);
}
Run Code Online (Sandbox Code Playgroud)

让我们逐步了解一下发生的事情:

  1. 在Main中,我们从Context0开始
  2. 因为Class2是a ContextBoundObject并且因为OhMyAttribute认为当前上下文不可接受,所以Class2Context1中创建了一个实例(我将调用它c2_real,并且返回并存储的c2是远程代理)c2_real.
  3. c2.Method1()被调用时,它被称为在远程代理.由于我们在Context0中,远程代理意识到它不在正确的上下文中,所以它切换到Context1,并Method1执行其中的代码.3.a在Method1我们内部调用NestedClass使用的构造函数c2.one.在这种情况下,我们已经在Context1中,因此c2.one不需要上下文切换,因此我们c2_real直接使用该对象.

现在,有问题的案例:

  1. 我们NestedClass在远程代理中创建一个新的传递c2.这里没有上下文切换,因为NestedClass不是a ContextBoundObject.
  2. NestedClassctor中,它访问c2.one.远程代理注意到我们仍然在Context0中,因此它尝试远程调用Context1.这是失败的,因为c2.one它是私有字段.你会看到Object.GetFieldInfo它只是在寻找公共领域:

    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;
    }
    
    Run Code Online (Sandbox Code Playgroud)

因此,如何async/ await最终导致同样的问题?

async/ await使您Class1得到重写,它使用了嵌套类状态机(用于ILSpy生成):

public class Class1 : ContextBoundObject
{
    // ...
    private struct <Method1>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.AwaitUnsafeOnCompleted<TaskAwaiter, Class1.<Method1>d__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.<Method1>d__0 <Method1>d__;
        <Method1>d__.<>4__this = this;
        <Method1>d__.<>t__builder = AsyncTaskMethodBuilder.Create();
        <Method1>d__.<>1__state = -1;
        AsyncTaskMethodBuilder <>t__builder = <Method1>d__.<>t__builder;
        <>t__builder.Start<Class1.<Method1>d__0>(ref <Method1>d__);
        return <Method1>d__.<>t__builder.Task;
    }
}
Run Code Online (Sandbox Code Playgroud)

需要注意的重要一点是

  • 它创建了一个嵌套结构,可以访问私有的 Class1
  • this变量被提升并存储在嵌套类中.

所以,这里发生的是

  1. 在最初调用c1.Method1()远程代理通知时,我们在Context0中,并且需要切换到Context1.
  2. 最终,MoveNext被称为,c1.one被称为.由于我们已经在Context1中,因此不需要进行上下文切换(因此不会出现问题).
  3. 之后,由于注册了延续,因此MoveNext将再次发出调用以执行其后的代码await.但是,这个调用MoveNext不会发生在对其中一个Class1方法的调用中.因此,当这次c1.one执行代码时,我们将在Context0中.远程代理通知我们在Context0中,并尝试上下文切换.由于c1.one是私有字段,因此导致与上述相同的故障.

解决方法: 我不确定一般的解决方法,但对于这种特定情况,您可以通过不使用this方法中的引用来解决此问题.即:

public async Task Method1()
{
    var temp = one;
    Console.WriteLine(temp);
    await Task.Delay(50);
    Console.WriteLine(temp);
}
Run Code Online (Sandbox Code Playgroud)

或者切换到使用私有属性而不是字段.