使用管道时一路异步
Async all the way when using pipeline
如果我有一个应用程序使用具有多个阶段的管道,在所有阶段使用 foreach
执行并调用:
CanExecute
Execute
界面是这样的:
public interface IService
{
bool CanExecute(IContext subject);
IContext Execute(IContext subject);
}
它基本上接受了一个上下文,returns一个变得更丰富的上下文。
在 Execute
方法的一个阶段中,我需要调用一个服务,并且想要进行异步操作。
所以现在 Execute
方法需要更改为例如
Task<IContext> ExecuteAsync(IContext subject);
使用 await
调用服务。
所有其他阶段都没有异步代码,但现在需要更改,因为最佳实践是 "async all the way"。
引入异步代码时必须进行这些更改是否正常?
Is this normal to have to make these changes when you bring in async code?
更改任何方法的签名时都必须进行更改是正常的。如果您想重命名它并更改 return 类型,那么是的,调用该方法的所有地方都必须更改。
改变它们的最好方法是让它们也异步,一直到链上。
C# 8 提供了多种方法来避免修改同步服务。 C# 7 也可以使用模式匹配语句来处理这个问题。
默认实现成员
接口版本控制是默认接口成员的主要用例之一。它们可用于避免在接口更改时更改现有 类。您可以为 ExecuteAsync
添加默认实现,即 returns Execute
的结果作为 ValueTask。
假设您有这些接口:
public interface IContext{}
public interface IService
{
public bool CanExecute(IContext subject);
public IContext Execute(IContext subject);
}
public class ServiceA:IService
{
public bool CanExecute(IContext subject)=>true;
public IContext Execute(IContext subject){return subject;}
}
要创建异步服务而不修改同步服务,您可以向 IService 添加默认实现并在新服务中覆盖它:
public interface IService
{
public bool CanExecute(IContext subject);
public IContext Execute(IContext subject);
public ValueTask<IContext> ExecuteAsync(IContext subject)=>new ValueTask<IContext>(Execute(subject));
}
public class ServiceB:IService
{
public bool CanExecute(IContext subject)=>true;
public IContext Execute(IContext subject)=>ExecuteAsync(subject).Result;
public async ValueTask<IContext> ExecuteAsync(IContext subject)
{
await Task.Yield();
return subject;
}
}
ServiceB.Execute
仍然需要一个 body 并且有意义的一件事是调用 ExecuteAsync()
并阻止,尽管看起来很丑陋。另一种可能性是如果 Execute
被调用则抛出:
public IContext Execute(IContext subject)=>throw new InvalidOperationException("This is an async service");
模式匹配
另一种选择是为异步服务创建第二个接口:
public interface IService
{
public bool CanExecute(IContext subject);
public IContext Execute(IContext subject);
}
public interface IServiceAsync:IService
{
public ValueTask<IContext> ExecuteAsync(IContext subject);
}
两个服务实现将保持不变。管道代码将根据服务类型进行更改以进行不同的调用:
async Task Main()
{
IService[] pipeline=new[]{(IService)new ServiceA(),new ServiceB()};
IContext ctx=new Context();
foreach(var svc in pipeline)
{
if (svc.CanExecute(ctx))
{
var result=svc switch { IServiceAsync a=>await a.ExecuteAsync(ctx),
IService b => b.Execute(ctx)};
ctx=result;
}
}
}
模式匹配表达式根据当前服务的类型调用不同的分支。对类型进行 Natching 会生成一个强类型实例(a 或 b),可用于调用适当的方法。
开关表达式是详尽无遗的 - 如果编译器无法验证所有选项都与模式匹配,则会生成警告。
C# 7
C# 7 没有 switch 表达式,因此需要更详细的模式匹配 switch 语句:
if (svc.CanExecute(ctx))
{
switch (svc)
{
case IServiceAsync a:
ctx=await a.ExecuteAsync(ctx);
break;
case IService b :
ctx=b.Execute(ctx);
break;
default:
throw new InvalidOperationException("Unknown service type!");
}
}
开关 语句 并不详尽,因此我们需要添加 default
部分以在运行时捕获错误。
如果我有一个应用程序使用具有多个阶段的管道,在所有阶段使用 foreach
执行并调用:
CanExecute
Execute
界面是这样的:
public interface IService
{
bool CanExecute(IContext subject);
IContext Execute(IContext subject);
}
它基本上接受了一个上下文,returns一个变得更丰富的上下文。
在 Execute
方法的一个阶段中,我需要调用一个服务,并且想要进行异步操作。
所以现在 Execute
方法需要更改为例如
Task<IContext> ExecuteAsync(IContext subject);
使用 await
调用服务。
所有其他阶段都没有异步代码,但现在需要更改,因为最佳实践是 "async all the way"。
引入异步代码时必须进行这些更改是否正常?
Is this normal to have to make these changes when you bring in async code?
更改任何方法的签名时都必须进行更改是正常的。如果您想重命名它并更改 return 类型,那么是的,调用该方法的所有地方都必须更改。
改变它们的最好方法是让它们也异步,一直到链上。
C# 8 提供了多种方法来避免修改同步服务。 C# 7 也可以使用模式匹配语句来处理这个问题。
默认实现成员
接口版本控制是默认接口成员的主要用例之一。它们可用于避免在接口更改时更改现有 类。您可以为 ExecuteAsync
添加默认实现,即 returns Execute
的结果作为 ValueTask。
假设您有这些接口:
public interface IContext{}
public interface IService
{
public bool CanExecute(IContext subject);
public IContext Execute(IContext subject);
}
public class ServiceA:IService
{
public bool CanExecute(IContext subject)=>true;
public IContext Execute(IContext subject){return subject;}
}
要创建异步服务而不修改同步服务,您可以向 IService 添加默认实现并在新服务中覆盖它:
public interface IService
{
public bool CanExecute(IContext subject);
public IContext Execute(IContext subject);
public ValueTask<IContext> ExecuteAsync(IContext subject)=>new ValueTask<IContext>(Execute(subject));
}
public class ServiceB:IService
{
public bool CanExecute(IContext subject)=>true;
public IContext Execute(IContext subject)=>ExecuteAsync(subject).Result;
public async ValueTask<IContext> ExecuteAsync(IContext subject)
{
await Task.Yield();
return subject;
}
}
ServiceB.Execute
仍然需要一个 body 并且有意义的一件事是调用 ExecuteAsync()
并阻止,尽管看起来很丑陋。另一种可能性是如果 Execute
被调用则抛出:
public IContext Execute(IContext subject)=>throw new InvalidOperationException("This is an async service");
模式匹配
另一种选择是为异步服务创建第二个接口:
public interface IService
{
public bool CanExecute(IContext subject);
public IContext Execute(IContext subject);
}
public interface IServiceAsync:IService
{
public ValueTask<IContext> ExecuteAsync(IContext subject);
}
两个服务实现将保持不变。管道代码将根据服务类型进行更改以进行不同的调用:
async Task Main()
{
IService[] pipeline=new[]{(IService)new ServiceA(),new ServiceB()};
IContext ctx=new Context();
foreach(var svc in pipeline)
{
if (svc.CanExecute(ctx))
{
var result=svc switch { IServiceAsync a=>await a.ExecuteAsync(ctx),
IService b => b.Execute(ctx)};
ctx=result;
}
}
}
模式匹配表达式根据当前服务的类型调用不同的分支。对类型进行 Natching 会生成一个强类型实例(a 或 b),可用于调用适当的方法。
开关表达式是详尽无遗的 - 如果编译器无法验证所有选项都与模式匹配,则会生成警告。
C# 7
C# 7 没有 switch 表达式,因此需要更详细的模式匹配 switch 语句:
if (svc.CanExecute(ctx))
{
switch (svc)
{
case IServiceAsync a:
ctx=await a.ExecuteAsync(ctx);
break;
case IService b :
ctx=b.Execute(ctx);
break;
default:
throw new InvalidOperationException("Unknown service type!");
}
}
开关 语句 并不详尽,因此我们需要添加 default
部分以在运行时捕获错误。