Получить идентификатор арендатора из сообщения RabbitMq для подключения к базе данных - PullRequest
0 голосов
/ 22 мая 2019

У меня есть микросервисная архитектура с приложениями ASP.Net Core и RabbitMq в качестве шины событий между микросервисами.
Я также хочу поддержать многопользовательскую аренду.
Поэтому у меня есть следующая служба внедрения зависимостей, определенная в Startup.cs, чтобы открывать соединение с базой данных при каждом запросе на основе идентификатора клиента пользователя.

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?

Или перефразируя мой вопрос: Есть ли способ получить доступ к сообщению RabbitMQ (и, например, к его заголовкам) при выполнении кода внедрения зависимости?

Ответы [ 2 ]

3 голосов
/ 29 мая 2019

Поскольку 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 я отправляю идентификатор арендатора через заголовки следующим образом:

                    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 (см. Мой вопрос)Я могу получить доступ к идентификатору клиента из заголовков сообщения:

    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();
    });
0 голосов
/ 27 мая 2019

Вы не можете

Или, может быть, но если ваш дизайн зависит от HTTP-контекста. В документации .NET о сроке службы указано:

Сервисы с ограниченным сроком службы создаются один раз для каждого запроса клиента. (Соединение).

Таким образом, с точки зрения вашей (HTTP) службы, запрос является точкой входа, которая использовала магию контейнера для того, чтобы с помощью глобального контекста HTTP настроить вашу базу данных для запроса перед любая ваша бизнес логика. Это не лучший вариант для разработки, особенно если вы планируете использовать эту же логику вне HTTP-запроса.

Напротив, ваша служба поддержки сообщений работает долго; В этом жизненном цикле, если для настройки вашего соединения требуется информация из каждого сообщения (идентификатор клиента), вы не можете полагаться исключительно на внедрение зависимостей.

«Правильным» способом было бы не полагаться на глобальное состояние в контексте HTTP для настройки соединения с базой данных. Вместо этого настройте контекст базы данных, который будет работать для всех ваших арендаторов.

...