private async Task OnMessageReceivedAsync <TEvent, TConsumer>(EventRegistration ereg, EventConsumerRegistration creg, IModel channel, BasicDeliverEventArgs args, CancellationToken cancellationToken) where TEvent : class where TConsumer : IEventConsumer <TEvent> { var messageId = args.BasicProperties?.MessageId; using var log_scope = Logger.BeginScopeForConsume(id: messageId, correlationId: args.BasicProperties?.CorrelationId, extras: new Dictionary <string, string> { ["RoutingKey"] = args.RoutingKey, ["DeliveryTag"] = args.DeliveryTag.ToString(), }); args.BasicProperties.Headers.TryGetValue(AttributeNames.ActivityId, out var parentActivityId); // Instrumentation using var activity = EventBusActivitySource.StartActivity(ActivityNames.Consume, ActivityKind.Consumer, parentActivityId?.ToString()); activity?.AddTag(ActivityTagNames.EventBusEventType, typeof(TEvent).FullName); activity?.AddTag(ActivityTagNames.EventBusConsumerType, typeof(TConsumer).FullName); activity?.AddTag(ActivityTagNames.MessagingSystem, Name); activity?.AddTag(ActivityTagNames.MessagingDestination, creg.ConsumerName); activity?.AddTag(ActivityTagNames.MessagingDestinationKind, "queue"); // only queues are possible try { Logger.LogDebug("Processing '{MessageId}'", messageId); using var scope = CreateScope(); using var ms = new MemoryStream(args.Body.ToArray()); var contentType = GetContentType(args.BasicProperties); var context = await DeserializeAsync <TEvent>(body : ms, contentType : contentType, registration : ereg, scope : scope, cancellationToken : cancellationToken); Logger.LogInformation("Received message: '{MessageId}' containing Event '{Id}'", messageId, context.Id); await ConsumeAsync <TEvent, TConsumer>(@event : context, scope : scope, cancellationToken : cancellationToken); // Acknowlege the message Logger.LogDebug("Completing message: {MessageId}, {DeliveryTag}.", messageId, args.DeliveryTag); channel.BasicAck(deliveryTag: args.DeliveryTag, multiple: false); } catch (Exception ex) { Logger.LogError(ex, "Event processing failed. Moving to deadletter."); channel.BasicNack(deliveryTag: args.DeliveryTag, multiple: false, requeue: false); } }
/// <summary> /// Get the consumer registration in a given event type. /// </summary> /// <typeparam name="TEvent">The event type from wich to retrieve a <see cref="EventConsumerRegistration"/> for.</typeparam> /// <typeparam name="TConsumer">The consumer to configure.</typeparam> /// <param name="registration"> /// When this method returns, contains the consumer registration associated with the specified event type, /// if the event type is found; otherwise, <see langword="null"/> is returned. /// This parameter is passed uninitialized. /// </param> /// <returns><see langword="true" /> if there's a consumer registered for the given event type; otherwise, false.</returns> public bool TryGetConsumerRegistration <TEvent, TConsumer>(out EventConsumerRegistration registration) { registration = default; if (Registrations.TryGetValue(typeof(TEvent), out var ereg)) { registration = ereg.Consumers.SingleOrDefault(cr => cr.ConsumerType == typeof(TConsumer)); if (registration != null) { return(true); } } return(false); }
private async Task OnMessageReceivedAsync <TEvent, TConsumer>(EventRegistration ereg, EventConsumerRegistration creg, ProcessMessageEventArgs args) where TEvent : class where TConsumer : IEventConsumer <TEvent> { var message = args.Message; var messageId = message.MessageId; var cancellationToken = args.CancellationToken; message.ApplicationProperties.TryGetValue(AttributeNames.ActivityId, out var parentActivityId); using var log_scope = Logger.BeginScopeForConsume(id: messageId, correlationId: message.CorrelationId, sequenceNumber: message.SequenceNumber, extras: new Dictionary <string, string> { ["EnqueuedSequenceNumber"] = message.EnqueuedSequenceNumber.ToString(), }); // Instrumentation using var activity = EventBusActivitySource.StartActivity(ActivityNames.Consume, ActivityKind.Consumer, parentActivityId?.ToString()); activity?.AddTag(ActivityTagNames.EventBusEventType, typeof(TEvent).FullName); activity?.AddTag(ActivityTagNames.EventBusConsumerType, typeof(TConsumer).FullName); activity?.AddTag(ActivityTagNames.MessagingSystem, Name); var destination = TransportOptions.UseBasicTier || ereg.UseQueueInsteadOfTopic() ? ereg.EventName : creg.ConsumerName; activity?.AddTag(ActivityTagNames.MessagingDestination, destination); // name of the queue/subscription activity?.AddTag(ActivityTagNames.MessagingDestinationKind, "queue"); // the spec does not know subscription so we can only use queue for both try { Logger.LogDebug("Processing '{MessageId}'", messageId); using var scope = CreateScope(); using var ms = message.Body.ToStream(); var contentType = new ContentType(message.ContentType); var context = await DeserializeAsync <TEvent>(body : ms, contentType : contentType, registration : ereg, scope : scope, cancellationToken : cancellationToken); Logger.LogInformation("Received message: '{MessageId}' containing Event '{Id}'", messageId, context.Id); await ConsumeAsync <TEvent, TConsumer>(@event : context, scope : scope, cancellationToken : cancellationToken); // Complete the message Logger.LogDebug("Completing message: {MessageId}.", messageId); await args.CompleteMessageAsync(message : message, cancellationToken : cancellationToken); } catch (Exception ex) { Logger.LogError(ex, "Event processing failed. Moving to deadletter."); await args.DeadLetterMessageAsync(message : message, cancellationToken : cancellationToken); } }
private async Task <ServiceBusProcessor> GetProcessorAsync(EventRegistration ereg, EventConsumerRegistration creg, CancellationToken cancellationToken) { await processorsCacheLock.WaitAsync(cancellationToken); try { var topicName = ereg.EventName; var subscriptionName = creg.ConsumerName; var key = $"{topicName}/{subscriptionName}"; if (!processorsCache.TryGetValue(key, out var processor)) { // Create the processor options var sbpo = TransportOptions.CreateProcessorOptions?.Invoke() ?? new ServiceBusProcessorOptions(); // Maximum number of concurrent calls to the callback ProcessMessagesAsync(), set to 1 for simplicity. // Set it according to how many messages the application wants to process in parallel. sbpo.MaxConcurrentCalls = 1; // Indicates whether MessagePump should automatically complete the messages after returning from User Callback. // False below indicates the Complete will be handled by the User Callback as in `ProcessMessagesAsync` below. sbpo.AutoCompleteMessages = false; // Create the processor. Queues are used in the basic tier or when explicitly mapped to Queue. // Otherwise, Topics and Subscriptions are used. if (TransportOptions.UseBasicTier || ereg.UseQueueInsteadOfTopic()) { // Ensure Queue is created await CreateQueueIfNotExistsAsync(name : topicName, cancellationToken : cancellationToken); // Create the processor for the Queue Logger.LogDebug("Creating processor for queue '{QueueName}'", topicName); processor = serviceBusClient.CreateProcessor(queueName: topicName, options: sbpo); } else { // Ensure Topic is created before creating the Subscription await CreateTopicIfNotExistsAsync(name : topicName, cancellationToken : cancellationToken); // Ensure Subscription is created await CreateSubscriptionIfNotExistsAsync(topicName : topicName, subscriptionName : subscriptionName, cancellationToken : cancellationToken); // Create the processor for the Subscription Logger.LogDebug("Creating processor for topic '{TopicName}' and subscription '{Subscription}'", topicName, subscriptionName); processor = serviceBusClient.CreateProcessor(topicName: topicName, subscriptionName: subscriptionName, options: sbpo); } processorsCache[key] = processor; } return(processor); } finally { processorsCacheLock.Release(); } }