从RabbitMq消息获取房客ID以进行Db连接

时间:2019-05-22 16:47:57

标签: events asp.net-core dependency-injection rabbitmq multi-tenant

我有一个微服务架构,具有ASP.Net Core应用程序和RabbitMq作为微服务之间的事件总线。
我也想支持多租户。
因此,我遵循了Startup.cs中定义的依赖项注入服务,以根据用户的租户ID在每个请求上打开与数据库的连接。

services.AddScoped<IDocumentSession>(ds =>
            {
                var store = ds.GetRequiredService<IDocumentStore>();
                var httpContextAccessor = ds.GetRequiredService<IHttpContextAccessor>();
                var tenant = httpContextAccessor?.HttpContext?.User?.Claims.FirstOrDefault(c => c.Type == "tid")?.Value;
                return tenant != null ? store.OpenSession(tenant) : store.OpenSession();
            });

问题在于服务何时处理事件总线消息(如UserUpdatedEvent)。
在这种情况下,当它尝试打开Db连接时,显然没有来自http上下文的用户信息。

在注入作用域服务并使用RabbitMq处理事件时,如何发送/访问相应用户的租户ID?

或改写我的问题: 在执行依赖项注入代码时,有什么方法可以访问RabbitMQ消息(例如其标头)?

2 个答案:

答案 0 :(得分:2)

由于没有HttpContext,因为RabbitMq请求不是Http请求(如@istepaniuk的回答所指出的那样),所以我创建了自己的上下文并将其命名为AmqpContext

public interface IAmqpContext
    {
        void ClearHeaders();
        void AddHeaders(IDictionary<string, object> headers);
        string GetHeaderByKey(string headerKey);
    }

    public class AmqpContext : IAmqpContext
    {
        private readonly Dictionary<string, object> _headers;

        public AmqpContext()
        {
            _headers = new Dictionary<string, object>();
        }

        public void ClearHeaders()
        {
            _headers.Clear();
        }

        public void AddHeaders(IDictionary<string, object> headers)
        {
            foreach (var header in headers)
                _headers.Add(header.Key, header.Value);
        }

        public string GetHeaderByKey(string headerKey) 
        {
            if (_headers.TryGetValue(headerKey, out object headerValue))
            {
                return Encoding.Default.GetString((byte[])headerValue);
            }
            return null;
        }
    }

当发送RabbitMq消息时,我通过如下标题发送租户ID:

                    var properties = channel.CreateBasicProperties();
                    if (tenantId != null)
                    {
                        var headers = new Dictionary<string, object>
                        {
                            { "tid", tenantId }
                        };
                        properties.Headers = headers;
                    }

                    channel.BasicPublish(exchange: BROKER_NAME,
                                     routingKey: eventName,
                                     mandatory: true,
                                     basicProperties: properties,
                                     body: body);

然后在接收服务上,我在AmqpContext中将Startup.cs注册为范围服务:

services.AddScoped<IAmqpContext, AmqpContext>();

在使用方通道中接收RabbitMq消息时,将创建范围和Amqp上下文:

consumer.Received += async (model, ea) =>
            {
                var eventName = ea.RoutingKey;
                var message = Encoding.UTF8.GetString(ea.Body);
                var properties = ea.BasicProperties;

                using (var scope = _serviceProvider.CreateScope())
                        {
                            var amqpContext = scope.ServiceProvider.GetService<IAmqpContext>();
                            if (amqpContext != null)
                            {
                                amqpContext.ClearHeaders();
                                if (properties.Headers != null && amqpContext != null)
                                {
                                    amqpContext.AddHeaders(properties.Headers);
                                }
                            }
                            var handler = scope.ServiceProvider.GetService(subscription.HandlerType);
                            if (handler == null) continue;
                            var eventType = _subsManager.GetEventTypeByName(eventName);
                            var integrationEvent = JsonConvert.DeserializeObject(message, eventType);
                            var concreteType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
                            await (Task)concreteType.GetMethod("Handle").Invoke(handler, new object[] { integrationEvent });
                        }

                channel.BasicAck(ea.DeliveryTag, multiple: false);
            };

然后,当创建作用域Db连接服务时(请参阅我的问题),我可以从消息头访问租户ID:

    services.AddScoped<IDocumentSession>(ds =>
    {
        var store = ds.GetRequiredService<IDocumentStore>();
        string tenant = null;
        var httpContextAccessor = ds.GetRequiredService<IHttpContextAccessor>();
        if (httpContextAccessor.HttpContext != null)
        {
            tenant = httpContextAccessor.HttpContext.User?.Claims.FirstOrDefault(c => c.Type == "tid")?.Value;
        }
        else
        {
            var amqpContext = ds.GetRequiredService<IAmqpContext>();
            tenant = amqpContext.GetHeaderByKey("tid");
        }
        return tenant != null ? store.OpenSession(tenant) : store.OpenSession();
    });

答案 1 :(得分:0)

你不能

也许,但是如果您的设计依赖于HTTP上下文,则不会。 .NET documentation on service lifetime指出:

  

范围内的生命周期服务每个客户请求创建一次   (连接)。

因此,从您的(HTTP)服务的角度来看,该请求是使用容器魔术的入口点,借助于全局HTTP上下文,每个请求,在您的任何业务逻辑之前。这似乎不是最佳的设计选择,尤其是如果您打算在HTTP请求之外使用相同的逻辑。

相比之下,您的消息消费者服务是长期运行的;在这个生命周期中,如果您的连接设置需要每条消息(租户ID)中的信息,您就不能仅依赖于依赖项注入。

“正确”的方法是不依赖HTTP上下文中的全局状态来建立数据库连接。设置一个适用于所有租户的数据库上下文。