我有一个微服务架构,具有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消息(例如其标头)?
答案 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上下文中的全局状态来建立数据库连接。设置一个适用于所有租户的数据库上下文。