本文介绍了在呼叫中将用户和审核信息传递给Service Fabric传输中的可靠服务的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

如何轻松地在客户端和服务之间传递审核信息,而不必将该信息添加为所有服务方法的参数?我可以使用邮件标题为呼叫设置此数据吗?

How can I pass along auditing information between clients and services in an easy way without having to add that information as arguments for all service methods? Can I use message headers to set this data for a call?

是否有一种方法可以允许服务沿着下游传递,即,如果ServiceA调用了ServiceB,而ServiceB调用了ServiceC,则是否可以将相同的审核信息发送到第一个A,然后在A的呼叫中发送给B,然后在B的呼叫中发送给C?

Is there a way to allow service to pass that along downstream also, i.e., if ServiceA calls ServiceB that calls ServiceC, could the same auditing information be send to first A, then in A's call to B and then in B's call to C?

推荐答案

如果您使用的是织物运输进行远程处理.如果您使用的是Http传输,那么那里就有标头,就像处理任何http请求一样.

There is actually a concept of headers that are passed between client and service if you are using fabric transport for remoting. If you are using Http transport then you have headers there just as you would with any http request.

请注意,下面的建议并不是最简单的解决方案,但是一旦解决就可以解决该问题,并且易于使用,但是,如果您希望在总体代码库中寻求轻松的解决方案,那么可能不是去.如果是这种情况,那么我建议您简单地向所有服务方法中添加一些通用的审核信息参数.当然,当一些开发人员忘记添加它或在调用下游服务时未正确设置它时,最需要注意的是.总是在代码中进行权衡取舍:).

Note, below proposal is not the easiest solution, but it solves the issue once it is in place and it is easy to use then, but if you are looking for easy in the overall code base this might not be the way to go. If that is the case then I suggest you simply add some common audit info parameter to all your service methods. The big caveat there is of course when some developer forgets to add it or it is not set properly when calling down stream services. It's all about trade-offs, as alway in code :).

顺着兔子洞

在结构传输中,通信涉及两个类: IServiceRemotingClient IServiceRemotingListener 在服务端.在客户端的每个请求中,消息主体 ServiceRemotingMessageHeaders 已发送.这些标头开箱即用地包含有关调用哪个接口(即哪个服务)和调用哪个方法的信息(这也是底层接收者如何知道如何解压缩作为主体的那个字节数组的信息)的信息.对于通过ActorService进行的Actor调用,这些标头中还包含其他Actor信息.

In fabric transport there are two classes that are involved in the communication: an instance of a IServiceRemotingClient on the client side, and an instance of IServiceRemotingListener on the service side. In each request from the client the messgae body and ServiceRemotingMessageHeaders are sent. Out of the box these headers include information of which interface (i.e. which service) and which method are being called (and that's also how the underlying receiver knows how to unpack that byte array that is the body). For calls to Actors, which goes through the ActorService, additional Actor information is also included in those headers.

棘手的部分是加入该交换并实际进行设置,然后读取其他标头.请在这里与我一起忍受,这是我们需要理解的一些与课程有关的课程.

The tricky part is hooking into that exchange and actually setting and then reading additional headers. Please bear with me here, it's a number of classes involved in this behind the curtains that we need to understand.

服务端

为服务(例如无状态服务的示例)设置IServiceRemotingListener时,通常使用便捷扩展方法,如下所示:

When you setup the IServiceRemotingListener for your service (example for a Stateless service) you usually use a convenience extension method, like so:

 protected override IEnumerable<ServiceInstanceListener> CreateServiceInstanceListeners()
 {
     yield return new ServiceInstanceListener(context => 
         this.CreateServiceRemotingListener(this.Context));
 }

(另一种方法是实现您自己的侦听器,但这并不是我们不愿在这里做的,我们只是不想在现有基础架构之上添加内容.请参见下文对于这种方法.)

在这里我们可以提供自己的侦听器,类似于扩展方法在幕后所做的事情.首先让我们看一下扩展方法的作用.它会在服务项目的程序集级别上寻找特定的属性: ServiceRemotingProviderAttribute .那个是abstract,但是您可以使用的一个,如果没有提供默认实例,它将得到 FabricTransportServiceRemotingProviderAttribute .在AssemblyInfo.cs(或任何其他文件,它是程序集属性)中进行设置:

This is where we can provide our own listener instead, similar to what that extention method does behind the curtains. Let's first look at what that extention method does. It goes looking for a specific attribute on assembly level on your service project: ServiceRemotingProviderAttribute. That one is abstract, but the one that you can use, and which you will get a default instance of, if none is provided, is FabricTransportServiceRemotingProviderAttribute. Set it in AssemblyInfo.cs (or any other file, it's an assembly attribute):

[assembly: FabricTransportServiceRemotingProvider()]

此属性具有两个有趣的可覆盖方法:

This attribute has two interesting overridable methods:

public override IServiceRemotingListener CreateServiceRemotingListener(
    ServiceContext serviceContext, IService serviceImplementation)
public override IServiceRemotingClientFactory CreateServiceRemotingClientFactory(
    IServiceRemotingCallbackClient callbackClient)

这两个方法负责创建侦听器和客户端工厂.这意味着事务的客户端也会对其进行检查.这就是为什么它是服务程序集在程序集级别上的一个属性的原因,客户端也可以将其与 IService 我们想要与之通信的客户端的派生接口.

These two methods are responsible for creating the the listener and the client factory. That means that it is also inspected by the client side of the transaction. That is why it is an attribute on assembly level for the service assembly, the client side can also pick it up together with the IService derived interface for the client we want to communicate with.

最终创建一个实例 FabricTransportServiceRemotingListener ,但是在此实现中,我们无法设置自己的特定 IServiceRemotingMessageHandler .如果您创建自己的FabricTransportServiceRemotingProviderAttribute子类并覆盖它,那么您实际上可以使其创建FabricTransportServiceRemotingListener的实例,该实例在构造函数中使用一个调度程序:

The CreateServiceRemotingListener ends up creating an instance FabricTransportServiceRemotingListener, however in this implementation we cannot set our own specific IServiceRemotingMessageHandler. If you create your own sub class of FabricTransportServiceRemotingProviderAttribute and override that then you can actually make it create an instance of FabricTransportServiceRemotingListener that takes in a dispatcher in the constructor:

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),如下所示:

The AuditableServiceRemotingDispatcher is where the magic happens. It is our own ServiceRemotingDispatcher subclass. Override the RequestResponseAsync (ignore HandleOneWay, it is not supported by service remoting, it throws an NotImplementedException if called), like this:

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实例:

Another, easier, but less flexible way, would be to directly create an instance of FabricTransportServiceRemotingListener with an instance of our custom dispatcher directly in the service:

 protected override IEnumerable<ServiceInstanceListener> CreateServiceInstanceListeners()
 {
     yield return new ServiceInstanceListener(context => 
         new FabricTransportServiceRemotingListener(this.Context, new AuditableServiceRemotingDispatcher(context, this)));
 }

为什么这不太灵活?好吧,因为使用属性也支持客户端,如下所示

客户端

好,现在我们可以在接收消息时读取自定义标头了,如何设置这些标头呢?让我们看看该属性的另一种方法:

Ok, so now we can read custom headers when receiving messages, how about setting those? Let's look at the other method of that attribute:

public override IServiceRemotingClientFactory CreateServiceRemotingClientFactory(IServiceRemotingCallbackClient callbackClient)
{
    return (IServiceRemotingClientFactory)new FabricTransportServiceRemotingClientFactory(
        callbackClient: callbackClient,
        servicePartitionResolver: (IServicePartitionResolver)null,
        traceId: (string)null);
}

在这里,我们不能只注入特定的处理程序或与服务类似的处理程序,我们必须提供自己的自定义工厂.为了不必重新实现 FabricTransportServiceRemotingClientFactory 我只是将其封装在我自己的 IServiceRemotingClientFactory :

Here we cannot just inject a specific handler or similar as for the service, we have to supply our own custom factory. In order not to have to reimplement the particulars of FabricTransportServiceRemotingClientFactory I simply encapsulate it in my own implementation of 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 :

This implementation simply passes along anything heavy lifting to the underlying factory, while returning it's own auditable client that similarily encapsulates a 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; }
    }
}

现在,我们在这里(最终)在这里设置了我们想要传递给服务的审核名称.

Now, in here is where we actually (and finally) set the audit name that we want to pass along to the service.

呼叫链和服务请求上下文

难题的最后一部分是ServiceRequestContext,这是一个自定义类,允许我们处理服务请求调用的环境上下文.这是相关的,因为它为我们提供了一种在调用链中传播上下文信息(如用户或关联ID(或我们希望在客户端和服务之间传递的任何其他标头信息))的简便方法.实现ServiceRequestContext如下:

One final piece of the puzzle, the ServiceRequestContext, which is a custom class that allows us to handle an ambient context for a service request call. This is relevant because it gives us an easy way to propagate that context information, like the user or a correlation id (or any other header information we want to pass between client and service), in a chain of calls. The implementation ServiceRequestContext looks like:

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>();
    }
}

最后一部分受Stephen Cleary的 SO答案的影响很大.它为我们提供了一种简单的方法来处理大量的呼叫中的环境信息,无论它们在Task上是同步还是异步.现在,有了这种方式,我们也可以在服务端的Dispatcher中设置该信息:

This last part was much influenced by the SO answer by Stephen Cleary. It gives us an easy way to handle the ambient information down a hierarcy of calls, weather they are synchronous or asyncronous over Tasks. Now, with this we have a way of setting that information also in the Dispatcher on the service side:

    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()只是获取和解压缩由客户端设置的标头的辅助方法)

(GetUser() and GetCorrelationId() are just helper methods that gets and unpacks the headers set by the client)

将其保留在适当的位置意味着该服务为任何常规呼叫创建的任何新客户端也将设置sam标头,因此在场景ServiceA-> ServiceB-> ServiceC中,我们仍将在呼叫中设置相同的用户从ServiceB到ServiceC.

Having this in place means that any new client created by the service for any aditional call will also have the sam headers set, so in the scenario ServiceA -> ServiceB -> ServiceC we will still have the same user set in the call from ServiceB to ServiceC.

什么?这么容易吗?是;)

从服务(例如无状态OWIN Web api)内部,首先捕获用户信息,然后创建ServiceProxyFactory的实例,并将该调用包装在ServiceRequestContext中:

From inside a service, for instance a Stateless OWIN web api, where you first capture the user information, you create an instance of ServiceProxyFactoryand wrap that call in a 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);

好吧,总而言之-您可以连接到服务远程处理以设置您自己的标头.正如我们在上面看到的,需要做一些工作来建立适当的机制,主要是创建您自己的基础结构子类.好的一面是,一旦您准备好了这些,便可以通过一种非常简单的方法来审核您的服务呼叫.

Ok, so to sum it up - you can hook into the service remoting to set your own headers. As we see above there is some work that needs to be done to get a mechanism for that in place, mainly creating your own subclasses of the underlying infrastructure. The upside is that once you have this in place, then you have a very easy way for auditing your service calls.

这篇关于在呼叫中将用户和审核信息传递给Service Fabric传输中的可靠服务的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

10-22 21:52