使用管道时始终异步



如果我有一个应用程序,它使用一个包含许多阶段的管道,在所有阶段上使用foreach执行,并调用:

CanExecute
Execute

接口是这样的:

public interface IService
{
bool CanExecute(IContext subject);
IContext Execute(IContext subject);
}

它基本上接受一个上下文,并返回一个它变得更丰富的上下文。

在其中一个阶段Execute方法中,我需要调用一个服务,并希望执行异步操作。因此,现在Execute方法需要更改为例如

Task<IContext> ExecuteAsync(IContext subject);

其中CCD_ 4用于对服务的呼叫。

所有其他阶段都没有异步代码,但现在需要更改,因为最佳实践是"一路异步"。

当引入异步代码时,必须进行这些更改是正常的吗?

C#8提供了多种方法来避免修改同步服务。C#7还可以通过模式匹配语句来处理这一问题。

默认实施成员

接口版本控制是默认接口成员的主要用例之一。它们可以用来避免在接口更改时更改现有类。您可以为ExecuteAsync添加一个默认实现,该实现将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仍然需要一个实体,有一件事是有意义的,那就是调用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;
}
}
}

模式匹配表达式根据当前服务的类型调用不同的分支。对类型进行Nathing会生成一个强类型实例(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!");
}
}

Switch语句并不详尽,因此我们需要添加default部分以在运行时捕获错误。

引入异步代码时必须进行这些更改是正常的吗?

当您更改任何方法的签名时,必须进行更改是正常的。如果您想重命名它并更改返回类型,那么是的,该方法调用的所有地方都必须更改。

改变它们的最好方法是让它们也异步,一直到链的上游。

相关内容

  • 没有找到相关文章

最新更新