我有一些使用ContextBoundObject和ContextAttribute编写拦截方法调用的日志代码。该代码基于代码项目sample。
这一切都运行良好,直到我们开始使用此库以及利用异步和等待的代码。现在我们在运行代码时遇到了远程错误。这是一个重现问题的简单示例:
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);
}
}
当我们调用Method1
时,我们会在第二个RemotingException
上获得以下Console.WriteLine
:
Remoting cannot find field 'one' on type 'WindowsFormsApplication1.Class1'.
有没有办法使用内置的C#方法解决这个问题,还是我们必须看一下像PostSharp这样的替代解决方案?
答案 0 :(得分:7)
简答:远程调用不适用于私有字段。 async
/ await
重写导致尝试在私有字段上进行远程调用。
可以在没有async
/ await
的情况下重现该问题。以这种方式展示它有助于理解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);
}
让我们逐步了解一下:
Class2
是ContextBoundObject
,并且由于OhMyAttribute
认为当前上下文不可接受,因此在{strong> Context1 中创建Class2
的实例(我将其称为c2_real
,并且c2
中返回并存储的内容是c2_real
的远程代理。c2.Method1()
时,会在远程代理上调用它。由于我们在Context0中,远程代理意识到它不在正确的上下文中,因此它切换到Context1,并执行Method1
中的代码。
3.a在Method1
内,我们调用使用NestedClass
的{{1}}构造函数。在这种情况下,我们已经在Context1中,因此c2.one
不需要上下文切换,因此我们直接使用c2.one
对象。现在,有问题的案例:
c2_real
中创建新的NestedClass
传递。此处不会发生上下文切换,因为c2
不是NestedClass
。 在ContextBoundObject
ctor中,它访问c2.one。远程代理注意到我们仍然在Context0中,因此它尝试远程调用Context1。此操作失败,因为NestedClass
是私有字段。您会在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;
}
/ async
如何导致同样的问题?
await
/ async
会导致您的await
被重写,以便它使用带状态机的嵌套类(使用ILSpy生成):
Class1
需要注意的重要事项是
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;
}
}
Class1
变量被提升并存储在嵌套类中。所以,这里发生的是
this
时,远程代理通知我们在Context0中,并且需要切换到Context1。 c1.Method1()
,并调用MoveNext
。由于我们已经在Context1中,因此不需要进行上下文切换(因此不会出现问题)。c1.one
以执行MoveNext
之后的其余代码。但是,在调用await
方法之一时,不会发生对MoveNext
的调用。因此,当这次执行代码Class1
时,我们将在Context0中。远程代理通知我们在Context0中,并尝试上下文切换。这导致与上述相同的失败,因为c1.one
是私有字段。 解决方法:强>
我不确定一般的解决方法,但对于这种特定情况,您可以通过不使用方法中的c1.one
引用来解决此问题。即:
this
或切换到使用私有属性而不是字段。
答案 1 :(得分:2)
这是一个更一般的解决方法。
它有以下不足之处:
SynchronizationContext
中的ContextBoundObject
。在这种情况下它会throw
。await
为空且 SynchronizationContext.Current
不是TaskScheduler.Current
时,它不支持使用TaskScheduler.Default
的情况。在这种情况下,通常await
会捕获TaskScheduler
,并使用它来发布剩余的工作,但由于此解决方案设置SynchronizationContext
,TaskScheduler
将无法捕获。因此,当检测到这种情况时,它将throw
。.ConfigureAwait(false)
,因为这会导致SynchronizationContext
无法捕获。不幸的是,我无法发现这种情况。但是,如果用户确实希望获得基础传递.ConfigureAwait(false)
的{{1}}行为,则可以使用自定义等待工具(请参阅https://stackoverflow.com/a/22417031/495262)。这里有一件有趣的事情是我试图创建一个“传递”SynchronizationContext
。也就是说,我不想覆盖任何现有的SynchronizationContext
,而是保留其行为和层次,以便在适当的上下文中完成工作。欢迎任何关于更好方法的评论。
SynchronizationContext