如何以简单的方式在客户端和服务之间传递审计信息,而无需将该信息作为所有服务方法的参数添加?我可以使用邮件标头为呼叫设置此数据吗?
是否有办法允许服务也沿着下游传递,即,如果ServiceA调用ServiceC调用ServiceC,可以将相同的审核信息发送到第一个A,然后在A的呼叫中发送给B,然后在B的呼叫中C 1
答案 0 :(得分:14)
如果您使用fabric transport进行远程处理,实际上有一个在客户端和服务之间传递的标头概念。如果您正在使用Http传输,那么您就可以像使用任何http请求那样拥有标头。
请注意,下面的提案不是最简单的解决方案,但它解决了问题,一旦它到位并且很容易使用,但如果你在整个代码库中寻找容易,这可能不是通往走。如果是这种情况,那么我建议您只需在所有服务方法中添加一些常见的审计信息参数。当然,当一些开发人员忘记添加它时,或者在调用流服务时未正确设置它时,一个重要的警告。这完全取决于代码中的权衡:)。
打下兔子洞
在结构传输中,通信涉及两个类:客户端的IServiceRemotingClient
实例和服务端的IServiceRemotingListener
实例。在来自客户端的每个请求中,发送了messgae body 和 ServiceRemotingMessageHeaders
。开箱即用的这些标题包括哪个接口(即哪个服务)和哪个方法被调用的信息(以及底层接收器如何知道如何解包作为主体的字节数组)。对于通过ActorService的Actors调用,其他Actor信息也包含在这些头文件中。
棘手的部分是挂钩进入该交换并实际设置然后阅读其他标题。请在这里忍受我,我们需要了解的幕后工作中涉及的一些课程。
服务方
当您为服务设置IServiceRemotingListener
时(无状态服务的示例),您通常使用便利扩展方法,如下所示:
protected override IEnumerable<ServiceInstanceListener> CreateServiceInstanceListeners()
{
yield return new ServiceInstanceListener(context =>
this.CreateServiceRemotingListener(this.Context));
}
(另一种方法是实现自己的倾听者,但这不是我们在这里做的事情,我们不想添加内容现有基础设施的顶部。请参阅下面的方法。)
这是我们可以提供自己的监听器的地方,类似于窗帘背后的扩展方法。让我们首先看一下这种扩展方法的作用。它会在您的服务项目上查找程序集级别的特定属性:ServiceRemotingProviderAttribute
。那个是abstract
,但是你可以使用的那个,如果没有提供,你将获得默认实例FabricTransportServiceRemotingProviderAttribute
。将其设置为AssemblyInfo.cs
(或任何其他文件,它是一个程序集属性):
[assembly: FabricTransportServiceRemotingProvider()]
此属性有两个有趣的可覆盖方法:
public override IServiceRemotingListener CreateServiceRemotingListener(
ServiceContext serviceContext, IService serviceImplementation)
public override IServiceRemotingClientFactory CreateServiceRemotingClientFactory(
IServiceRemotingCallbackClient callbackClient)
这两种方法负责创建监听器和客户端工厂。这意味着它也由交易的客户端进行检查。这就是为什么它是服务程序集的程序集级别的属性,客户端也可以与我们想要与之通信的客户端的IService
派生接口一起选择它。
CreateServiceRemotingListener
最终会创建一个实例FabricTransportServiceRemotingListener
,但在此实现中,我们无法设置我们自己的特定IServiceRemotingMessageHandler
。如果您创建自己的FabricTransportServiceRemotingProviderAttribute
子类并覆盖它,那么您实际上可以创建一个FabricTransportServiceRemotingListener
的实例,它接受构造函数中的调度程序:
public class AuditableFabricTransportServiceRemotingProviderAttribute :
FabricTransportServiceRemotingProviderAttribute
{
public override IServiceRemotingListener CreateServiceRemotingListener(
ServiceContext serviceContext, IService serviceImplementation)
{
var messageHandler = new AuditableServiceRemotingDispatcher(
serviceContext, serviceImplementation);
return (IServiceRemotingListener)new FabricTransportServiceRemotingListener(
serviceContext: serviceContext,
messageHandler: messageHandler);
}
}
AuditableServiceRemotingDispatcher
是神奇发生的地方。它是我们自己的ServiceRemotingDispatcher
子类。覆盖RequestResponseAsync
(忽略HandleOneWay
,服务远程处理不支持它,如果被调用则抛出NotImplementedException
,如下所示:
public class AuditableServiceRemotingDispatcher : ServiceRemotingDispatcher
{
public AuditableServiceRemotingDispatcher(ServiceContext serviceContext, IService service) :
base(serviceContext, service) { }
public override async Task<byte[]> RequestResponseAsync(
IServiceRemotingRequestContext requestContext,
ServiceRemotingMessageHeaders messageHeaders,
byte[] requestBodyBytes)
{
byte[] userHeader = null;
if (messageHeaders.TryGetHeaderValue("user-header", out auditHeader))
{
// Deserialize from byte[] and handle the header
}
else
{
// Throw exception?
}
byte[] result = null;
result = await base.RequestResponseAsync(requestContext, messageHeaders, requestBodyBytes);
return result;
}
}
另一种更简单但不太灵活的方法是直接在服务中直接创建一个FabricTransportServiceRemotingListener
实例,其中包含我们自定义调度程序的实例:
protected override IEnumerable<ServiceInstanceListener> CreateServiceInstanceListeners()
{
yield return new ServiceInstanceListener(context =>
new FabricTransportServiceRemotingListener(this.Context, new AuditableServiceRemotingDispatcher(context, this)));
}
为什么这不灵活?好吧,因为使用该属性也支持客户端,如下所示
客户端
好的,现在我们可以在接收邮件时阅读自定义标题,如何设置这些标题?让我们看一下该属性的另一种方法:
public override IServiceRemotingClientFactory CreateServiceRemotingClientFactory(IServiceRemotingCallbackClient callbackClient)
{
return (IServiceRemotingClientFactory)new FabricTransportServiceRemotingClientFactory(
callbackClient: callbackClient,
servicePartitionResolver: (IServicePartitionResolver)null,
traceId: (string)null);
}
这里我们不能只注入一个特定的处理程序或类似的服务,我们必须提供我们自己的自定义工厂。为了不必重新实现FabricTransportServiceRemotingClientFactory
的详细信息,我只是将其封装在我自己的IServiceRemotingClientFactory
实现中:
public class AuditedFabricTransportServiceRemotingClientFactory : IServiceRemotingClientFactory, ICommunicationClientFactory<IServiceRemotingClient>
{
private readonly ICommunicationClientFactory<IServiceRemotingClient> _innerClientFactory;
public AuditedFabricTransportServiceRemotingClientFactory(ICommunicationClientFactory<IServiceRemotingClient> innerClientFactory)
{
_innerClientFactory = innerClientFactory;
_innerClientFactory.ClientConnected += OnClientConnected;
_innerClientFactory.ClientDisconnected += OnClientDisconnected;
}
private void OnClientConnected(object sender, CommunicationClientEventArgs<IServiceRemotingClient> e)
{
EventHandler<CommunicationClientEventArgs<IServiceRemotingClient>> clientConnected = this.ClientConnected;
if (clientConnected == null) return;
clientConnected((object)this, new CommunicationClientEventArgs<IServiceRemotingClient>()
{
Client = e.Client
});
}
private void OnClientDisconnected(object sender, CommunicationClientEventArgs<IServiceRemotingClient> e)
{
EventHandler<CommunicationClientEventArgs<IServiceRemotingClient>> clientDisconnected = this.ClientDisconnected;
if (clientDisconnected == null) return;
clientDisconnected((object)this, new CommunicationClientEventArgs<IServiceRemotingClient>()
{
Client = e.Client
});
}
public async Task<IServiceRemotingClient> GetClientAsync(
Uri serviceUri,
ServicePartitionKey partitionKey,
TargetReplicaSelector targetReplicaSelector,
string listenerName,
OperationRetrySettings retrySettings,
CancellationToken cancellationToken)
{
var client = await _innerClientFactory.GetClientAsync(
serviceUri,
partitionKey,
targetReplicaSelector,
listenerName,
retrySettings,
cancellationToken);
return new AuditedFabricTransportServiceRemotingClient(client);
}
public async Task<IServiceRemotingClient> GetClientAsync(
ResolvedServicePartition previousRsp,
TargetReplicaSelector targetReplicaSelector,
string listenerName,
OperationRetrySettings retrySettings,
CancellationToken cancellationToken)
{
var client = await _innerClientFactory.GetClientAsync(
previousRsp,
targetReplicaSelector,
listenerName,
retrySettings,
cancellationToken);
return new AuditedFabricTransportServiceRemotingClient(client);
}
public Task<OperationRetryControl> ReportOperationExceptionAsync(
IServiceRemotingClient client,
ExceptionInformation exceptionInformation,
OperationRetrySettings retrySettings,
CancellationToken cancellationToken)
{
return _innerClientFactory.ReportOperationExceptionAsync(
client,
exceptionInformation,
retrySettings,
cancellationToken);
}
public event EventHandler<CommunicationClientEventArgs<IServiceRemotingClient>> ClientConnected;
public event EventHandler<CommunicationClientEventArgs<IServiceRemotingClient>> ClientDisconnected;
}
这个实现简单地将任何繁重的工作转移到底层工厂,同时返回它自己的可审计客户端,类似地封装了IServiceRemotingClient
:
public class AuditedFabricTransportServiceRemotingClient : IServiceRemotingClient, ICommunicationClient
{
private readonly IServiceRemotingClient _innerClient;
public AuditedFabricTransportServiceRemotingClient(IServiceRemotingClient innerClient)
{
_innerClient = innerClient;
}
~AuditedFabricTransportServiceRemotingClient()
{
if (this._innerClient == null) return;
var disposable = this._innerClient as IDisposable;
disposable?.Dispose();
}
Task<byte[]> IServiceRemotingClient.RequestResponseAsync(ServiceRemotingMessageHeaders messageHeaders, byte[] requestBody)
{
messageHeaders.SetUser(ServiceRequestContext.Current.User);
messageHeaders.SetCorrelationId(ServiceRequestContext.Current.CorrelationId);
return this._innerClient.RequestResponseAsync(messageHeaders, requestBody);
}
void IServiceRemotingClient.SendOneWay(ServiceRemotingMessageHeaders messageHeaders, byte[] requestBody)
{
messageHeaders.SetUser(ServiceRequestContext.Current.User);
messageHeaders.SetCorrelationId(ServiceRequestContext.Current.CorrelationId);
this._innerClient.SendOneWay(messageHeaders, requestBody);
}
public ResolvedServicePartition ResolvedServicePartition
{
get { return this._innerClient.ResolvedServicePartition; }
set { this._innerClient.ResolvedServicePartition = value; }
}
public string ListenerName
{
get { return this._innerClient.ListenerName; }
set { this._innerClient.ListenerName = value; }
}
public ResolvedServiceEndpoint Endpoint
{
get { return this._innerClient.Endpoint; }
set { this._innerClient.Endpoint = value; }
}
}
现在,在这里我们实际(并最终)设置我们想要传递给服务的审核名称。
调用链和服务请求上下文
最后一个难题,ServiceRequestContext,它是一个自定义类,允许我们处理服务请求调用的环境上下文。这是相关的,因为它为我们提供了一种简单的方法来传播上下文信息,例如用户或相关ID(或我们想要在客户端和服务之间传递的任何其他头信息),在一系列调用中。实施ServiceRequestContext
看起来像:
public sealed class ServiceRequestContext
{
private static readonly string ContextKey = Guid.NewGuid().ToString();
public ServiceRequestContext(Guid correlationId, string user)
{
this.CorrelationId = correlationId;
this.User = user;
}
public Guid CorrelationId { get; private set; }
public string User { get; private set; }
public static ServiceRequestContext Current
{
get { return (ServiceRequestContext)CallContext.LogicalGetData(ContextKey); }
internal set
{
if (value == null)
{
CallContext.FreeNamedDataSlot(ContextKey);
}
else
{
CallContext.LogicalSetData(ContextKey, value);
}
}
}
public static Task RunInRequestContext(Func<Task> action, Guid correlationId, string user)
{
Task<Task> task = null;
task = new Task<Task>(async () =>
{
Debug.Assert(ServiceRequestContext.Current == null);
ServiceRequestContext.Current = new ServiceRequestContext(correlationId, user);
try
{
await action();
}
finally
{
ServiceRequestContext.Current = null;
}
});
task.Start();
return task.Unwrap();
}
public static Task<TResult> RunInRequestContext<TResult>(Func<Task<TResult>> action, Guid correlationId, string user)
{
Task<Task<TResult>> task = null;
task = new Task<Task<TResult>>(async () =>
{
Debug.Assert(ServiceRequestContext.Current == null);
ServiceRequestContext.Current = new ServiceRequestContext(correlationId, user);
try
{
return await action();
}
finally
{
ServiceRequestContext.Current = null;
}
});
task.Start();
return task.Unwrap<TResult>();
}
}
最后一部分受到SO answer by Stephen Cleary的影响很大。它为我们提供了一种简单的方法来处理环境信息,这些信息可以通过呼叫,天气与任务同步或异步来处理。现在,通过这种方式,我们可以在服务端的Dispatcher中设置该信息:
public override Task<byte[]> RequestResponseAsync(
IServiceRemotingRequestContext requestContext,
ServiceRemotingMessageHeaders messageHeaders,
byte[] requestBody)
{
var user = messageHeaders.GetUser();
var correlationId = messageHeaders.GetCorrelationId();
return ServiceRequestContext.RunInRequestContext(async () =>
await base.RequestResponseAsync(
requestContext,
messageHeaders,
requestBody),
correlationId, user);
}
(GetUser()
和GetCorrelationId()
只是获取和解包客户端设置的标头的辅助方法。
实现此功能意味着服务为任何aditional调用创建的任何新客户端也将设置sam标头,因此在场景ServiceA中 - &gt; ServiceB - &gt; ServiceC我们仍然会在从ServiceB到ServiceC的呼叫中设置相同的用户。
是什么?那容易吗?是的;)
从服务内部,例如Stateless OWIN web api,您首先捕获用户信息,您创建ServiceProxyFactory
的实例并将该调用包装在ServiceRequestContext
中:
var task = ServiceRequestContext.RunInRequestContext(async () =>
{
var serviceA = ServiceProxyFactory.CreateServiceProxy<IServiceA>(new Uri($"{FabricRuntime.GetActivationContext().ApplicationName}/ServiceA"));
await serviceA.DoStuffAsync(CancellationToken.None);
}, Guid.NewGuid(), user);
好的,总而言之 - 您可以挂钩服务远程处理以设置自己的标头。正如我们在上面看到的那样,需要做一些工作来获得适当的机制,主要是创建自己的底层基础结构的子类。好处是,一旦你有了这个,那么你有一个非常简单的方法来审核你的服务电话。