重构代码并抽取基础构建库
This commit is contained in:
1 parent
9e6b5be92c
commit
ccf1e7672d
55 files changed
+398
-104
No files matched your search
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) HelloShop Corporation. All rights reserved.
|
||||
// See the license file in the project root for more information.
|
||||
|
||||
using Dapr.Client;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace HelloShop.DistributedLock.Dapr
|
||||
{
|
||||
public class DaprDistributedLock(DaprClient daprClient) : IDistributedLock
|
||||
{
|
||||
public async Task<IDistributedLockResult> LockAsync(string resourceId, int expiryInSeconds = default, CancellationToken cancellationToken = default)
|
||||
{
|
||||
expiryInSeconds = expiryInSeconds == default ? 60 : expiryInSeconds;
|
||||
|
||||
// The CallerMemberNameAttribute is used to get the name of the calling method.
|
||||
string? lockOwner = new StackTrace().GetFrame(1)?.GetMethod()?.DeclaringType?.Name;
|
||||
|
||||
#pragma warning disable CS0618
|
||||
TryLockResponse response = await daprClient.Lock("lockstore", resourceId, lockOwner, expiryInSeconds, cancellationToken);
|
||||
#pragma warning restore CS0618
|
||||
|
||||
return new DaprDistributedLockResult(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) HelloShop Corporation. All rights reserved.
|
||||
// See the license file in the project root for more information.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace HelloShop.DistributedLock.Dapr
|
||||
{
|
||||
public static class DaprDistributedLockExtensions
|
||||
{
|
||||
public static IServiceCollection AddDaprDistributedLock(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IDistributedLock, DaprDistributedLock>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) HelloShop Corporation. All rights reserved.
|
||||
// See the license file in the project root for more information.
|
||||
|
||||
using Dapr.Client;
|
||||
|
||||
namespace HelloShop.DistributedLock.Dapr
|
||||
{
|
||||
#pragma warning disable CS0618
|
||||
public class DaprDistributedLockResult(TryLockResponse tryLockResponse) : IDistributedLockResult
|
||||
#pragma warning restore CS0618
|
||||
{
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await tryLockResponse.DisposeAsync().ConfigureAwait(false);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapr.AspNetCore" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HelloShop.DistributedLock\HelloShop.DistributedLock.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) HelloShop Corporation. All rights reserved.
|
||||
// See the license file in the project root for more information.
|
||||
|
||||
namespace HelloShop.DistributedLock
|
||||
{
|
||||
public interface IDistributedLock
|
||||
{
|
||||
public abstract Task<IDistributedLockResult> LockAsync(string resourceId, int expiryInSeconds = default, CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) HelloShop Corporation. All rights reserved.
|
||||
// See the license file in the project root for more information.
|
||||
|
||||
namespace HelloShop.DistributedLock
|
||||
{
|
||||
public interface IDistributedLockResult : IAsyncDisposable { }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) HelloShop Corporation. All rights reserved.
|
||||
// See the license file in the project root for more information.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace HelloShop.EventBus.Abstractions
|
||||
{
|
||||
public record DistributedEvent
|
||||
{
|
||||
public DistributedEvent()
|
||||
{
|
||||
Id = Guid.NewGuid();
|
||||
CreationTime = TimeProvider.System.GetUtcNow();
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public DateTimeOffset CreationTime { get; set; }
|
||||
|
||||
[JsonExtensionData]
|
||||
public Dictionary<string, JsonElement>? ExtensionData { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) HelloShop Corporation. All rights reserved.
|
||||
// See the license file in the project root for more information.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace HelloShop.EventBus.Abstractions
|
||||
{
|
||||
public static class EventBusBuilderExtensions
|
||||
{
|
||||
public static IEventBusBuilder ConfigureJsonOptions(this IEventBusBuilder eventBusBuilder, Action<JsonSerializerOptions> configure)
|
||||
{
|
||||
eventBusBuilder.Services.Configure<EventBusOptions>(o =>
|
||||
{
|
||||
configure(o.JsonSerializerOptions);
|
||||
});
|
||||
|
||||
return eventBusBuilder;
|
||||
}
|
||||
|
||||
public static IEventBusBuilder AddSubscription<TEvent, TEventHandler>(this IEventBusBuilder eventBusBuilder) where TEvent : DistributedEvent where TEventHandler : class, IDistributedEventHandler<TEvent>
|
||||
{
|
||||
eventBusBuilder.Services.AddKeyedTransient<IDistributedEventHandler, TEventHandler>(typeof(TEvent));
|
||||
|
||||
eventBusBuilder.Services.Configure<EventBusOptions>(o =>
|
||||
{
|
||||
o.EventTypes[typeof(TEvent).Name] = typeof(TEvent);
|
||||
});
|
||||
|
||||
return eventBusBuilder;
|
||||
}
|
||||
|
||||
public static IEventBusBuilder AddSubscription(this IEventBusBuilder eventBusBuilder, Type eventType, Type eventHandlerType)
|
||||
{
|
||||
eventBusBuilder.Services.AddKeyedTransient(typeof(IDistributedEventHandler), eventType, eventHandlerType);
|
||||
|
||||
eventBusBuilder.Services.Configure<EventBusOptions>(o =>
|
||||
{
|
||||
o.EventTypes[eventType.Name] = eventType;
|
||||
});
|
||||
|
||||
return eventBusBuilder;
|
||||
}
|
||||
|
||||
public static IEventBusBuilder AddSubscriptionFromAssembly(this IEventBusBuilder eventBusBuilder, Assembly? assembly = null)
|
||||
{
|
||||
assembly ??= Assembly.GetCallingAssembly();
|
||||
|
||||
var handlers = assembly.GetTypes().Where(t => t.IsAssignableTo(typeof(IDistributedEventHandler))).ToList();
|
||||
|
||||
handlers.ForEach(handler =>
|
||||
{
|
||||
var eventType = handler.GetInterfaces().Single(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDistributedEventHandler<>)).GetGenericArguments().Single();
|
||||
eventBusBuilder.AddSubscription(eventType, handler);
|
||||
});
|
||||
|
||||
return eventBusBuilder;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) HelloShop Corporation. All rights reserved.
|
||||
// See the license file in the project root for more information.
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace HelloShop.EventBus.Abstractions
|
||||
{
|
||||
public class EventBusOptions
|
||||
{
|
||||
public Dictionary<string, Type> EventTypes { get; } = [];
|
||||
|
||||
public JsonSerializerOptions JsonSerializerOptions { get; } = new(DefaultSerializerOptions);
|
||||
|
||||
internal static readonly JsonSerializerOptions DefaultSerializerOptions = new()
|
||||
{
|
||||
TypeInfoResolver = JsonSerializer.IsReflectionEnabledByDefault ? new DefaultJsonTypeInfoResolver() : JsonTypeInfoResolver.Combine()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) HelloShop Corporation. All rights reserved.
|
||||
// See the license file in the project root for more information.
|
||||
|
||||
namespace HelloShop.EventBus.Abstractions
|
||||
{
|
||||
public interface IDistributedEventHandler
|
||||
{
|
||||
Task HandleAsync(DistributedEvent @event);
|
||||
}
|
||||
|
||||
public interface IDistributedEventHandler<in TDistributedEvent> : IDistributedEventHandler where TDistributedEvent : DistributedEvent
|
||||
{
|
||||
Task HandleAsync(TDistributedEvent @event);
|
||||
|
||||
Task IDistributedEventHandler.HandleAsync(DistributedEvent @event) => HandleAsync((TDistributedEvent)@event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) HelloShop Corporation. All rights reserved.
|
||||
// See the license file in the project root for more information.
|
||||
|
||||
namespace HelloShop.EventBus.Abstractions
|
||||
{
|
||||
public interface IEventBus
|
||||
{
|
||||
Task PublishAsync(DistributedEvent @event, CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) HelloShop Corporation. All rights reserved.
|
||||
// See the license file in the project root for more information.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace HelloShop.EventBus.Abstractions
|
||||
{
|
||||
public interface IEventBusBuilder
|
||||
{
|
||||
public IServiceCollection Services { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) HelloShop Corporation. All rights reserved.
|
||||
// See the license file in the project root for more information.
|
||||
|
||||
using Dapr;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace HelloShop.EventBus.Dapr
|
||||
{
|
||||
public class DaprCloudEvent<TData>(TData data) : CloudEvent<TData>(data)
|
||||
{
|
||||
/// <summary>
|
||||
/// CloudEvent 'pubsubname' attribute.
|
||||
/// </summary>
|
||||
[JsonPropertyName("pubsubname")]
|
||||
public required string PubSubName { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CloudEvent 'topic' attribute.
|
||||
/// </summary>
|
||||
[JsonPropertyName("topic")]
|
||||
public required string Topic { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CloudEvent 'time' attribute.
|
||||
/// </summary>
|
||||
[JsonPropertyName("time")]
|
||||
public required DateTimeOffset Time { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) HelloShop Corporation. All rights reserved.
|
||||
// See the license file in the project root for more information.
|
||||
|
||||
using Dapr.Client;
|
||||
using HelloShop.EventBus.Abstractions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace HelloShop.EventBus.Dapr
|
||||
{
|
||||
public class DaprEventBus(DaprClient daprClient, IOptions<DaprEventBusOptions> options, ILogger<DaprEventBus> logger) : IEventBus
|
||||
{
|
||||
public async Task PublishAsync(DistributedEvent @event, CancellationToken cancellationToken = default)
|
||||
{
|
||||
string pubSubName = options.Value.PubSubName;
|
||||
string topicName = @event.GetType().Name;
|
||||
|
||||
logger.LogInformation("Publishing event {@Event} to {PubsubName}.{TopicName}", @event, pubSubName, topicName);
|
||||
|
||||
object? data = Convert.ChangeType(@event, @event.GetType());
|
||||
|
||||
await daprClient.PublishEventAsync(pubSubName, topicName, data, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) HelloShop Corporation. All rights reserved.
|
||||
// See the license file in the project root for more information.
|
||||
|
||||
using HelloShop.EventBus.Abstractions;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace HelloShop.EventBus.Dapr
|
||||
{
|
||||
public static class DaprEventBusExtensions
|
||||
{
|
||||
private const string DefaultSectionName = "DaprEventBus";
|
||||
|
||||
public static IEventBusBuilder AddDaprEventBus(this IHostApplicationBuilder builder, string sectionName = DefaultSectionName)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
|
||||
builder.Services.AddDaprClient();
|
||||
|
||||
DaprEventBusOptions daprOptions = new();
|
||||
|
||||
builder.Configuration.GetSection(sectionName).Bind(daprOptions);
|
||||
|
||||
builder.Services.AddSingleton(Options.Create(daprOptions));
|
||||
|
||||
builder.Services.AddSingleton<IEventBus, DaprEventBus>();
|
||||
|
||||
if (daprOptions.RequireAuthenticatedDaprApiToken)
|
||||
{
|
||||
builder.Services.AddAuthentication().AddDapr();
|
||||
builder.Services.Configure<AuthorizationOptions>(options => options.AddDapr());
|
||||
}
|
||||
|
||||
return new DistributedEventBusBuilder(builder.Services);
|
||||
}
|
||||
|
||||
private class DistributedEventBusBuilder(IServiceCollection services) : IEventBusBuilder
|
||||
{
|
||||
public IServiceCollection Services => services;
|
||||
}
|
||||
|
||||
public static WebApplication MapDaprEventBus(this WebApplication app)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(app);
|
||||
|
||||
var eventBusOptions = app.Services.GetRequiredService<IOptions<EventBusOptions>>().Value;
|
||||
|
||||
var daprEventBusOptions = app.Services.GetRequiredService<IOptions<DaprEventBusOptions>>().Value;
|
||||
|
||||
RouteHandlerBuilder routeHandler = app.MapPost($"/api/DistributedEvents", async (DaprCloudEvent<JsonElement> cloudEvent, IHttpContextAccessor contextAccessor) =>
|
||||
{
|
||||
var httpContext = contextAccessor.HttpContext ?? throw new InvalidOperationException("HTTP context not available.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cloudEvent.Topic) || !eventBusOptions.EventTypes.TryGetValue(cloudEvent.Topic, out Type? eventType) || eventType is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var jsonOptions = httpContext.RequestServices.GetRequiredService<IOptions<Microsoft.AspNetCore.Mvc.JsonOptions>>().Value;
|
||||
|
||||
if (JsonSerializer.Deserialize(cloudEvent.Data.GetRawText(), eventType, jsonOptions.JsonSerializerOptions) is not DistributedEvent @event)
|
||||
{
|
||||
return Results.BadRequest();
|
||||
}
|
||||
|
||||
foreach (var handler in httpContext.RequestServices.GetKeyedServices<IDistributedEventHandler>(eventType))
|
||||
{
|
||||
await handler.HandleAsync(@event);
|
||||
}
|
||||
|
||||
return Results.Ok();
|
||||
|
||||
}).WithTags(nameof(DistributedEvent));
|
||||
|
||||
app.MapSubscribeHandler();
|
||||
|
||||
foreach (var subscription in eventBusOptions.EventTypes)
|
||||
{
|
||||
routeHandler.WithTopic(daprEventBusOptions.PubSubName, subscription.Key, enableRawPayload: false);
|
||||
}
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) HelloShop Corporation. All rights reserved.
|
||||
// See the license file in the project root for more information.
|
||||
|
||||
namespace HelloShop.EventBus.Dapr
|
||||
{
|
||||
public class DaprEventBusOptions
|
||||
{
|
||||
public string PubSubName { get; set; } = "event-bus-pubsub";
|
||||
|
||||
public bool RequireAuthenticatedDaprApiToken { get; set; } = false;
|
||||
|
||||
public string? DaprApiToken { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapr.AspNetCore" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HelloShop.EventBus.Abstractions\HelloShop.EventBus.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Aspire.RabbitMQ.Client" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HelloShop.EventBus.Abstractions\HelloShop.EventBus.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) HelloShop Corporation. All rights reserved.
|
||||
// See the license file in the project root for more information.
|
||||
|
||||
using HelloShop.EventBus.Abstractions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Polly;
|
||||
using Polly.Retry;
|
||||
using RabbitMQ.Client;
|
||||
using RabbitMQ.Client.Events;
|
||||
using RabbitMQ.Client.Exceptions;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace HelloShop.EventBus.RabbitMQ
|
||||
{
|
||||
public sealed class RabbitMQEventBus(ILogger<RabbitMQEventBus> logger, IServiceProvider serviceProvider, IOptions<RabbitMQEventBusOptions> rabbitMQEventBusOptions, IOptions<EventBusOptions> eventBusOptions) : IEventBus, IDisposable, IHostedService
|
||||
{
|
||||
private readonly ResiliencePipeline _pipeline = CreateResiliencePipeline(rabbitMQEventBusOptions.Value.RetryCount);
|
||||
private readonly string _queueName = rabbitMQEventBusOptions.Value.QueueName;
|
||||
private readonly string _exchangeName = rabbitMQEventBusOptions.Value.ExchangeName;
|
||||
private readonly EventBusOptions _eventBusOptions = eventBusOptions.Value;
|
||||
|
||||
private IConnection? _rabbitMQConnection;
|
||||
private IModel? _consumerChannel;
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
_rabbitMQConnection = serviceProvider.GetRequiredService<IConnection>();
|
||||
_consumerChannel = _rabbitMQConnection.CreateModel();
|
||||
_consumerChannel.ExchangeDeclare(exchange: _exchangeName, type: ExchangeType.Direct);
|
||||
_consumerChannel.QueueDeclare(queue: _queueName, durable: true, exclusive: false, autoDelete: false, arguments: null);
|
||||
|
||||
var consumer = new AsyncEventingBasicConsumer(_consumerChannel);
|
||||
consumer.Received += OnMessageReceivedAsync;
|
||||
_consumerChannel.BasicConsume(queue: _queueName, autoAck: false, consumer: consumer);
|
||||
|
||||
foreach (var (eventName, _) in _eventBusOptions.EventTypes)
|
||||
{
|
||||
_consumerChannel.QueueBind(queue: _queueName, exchange: _exchangeName, routingKey: eventName);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "An error occurred while starting the RabbitMQ event bus.");
|
||||
}
|
||||
}, TaskCreationOptions.LongRunning);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
public void Dispose() => _consumerChannel?.Dispose();
|
||||
|
||||
public Task PublishAsync(DistributedEvent @event, CancellationToken cancellationToken = default)
|
||||
{
|
||||
string routingKey = @event.GetType().Name;
|
||||
|
||||
using var channel = _rabbitMQConnection?.CreateModel() ?? throw new InvalidOperationException("RabbitMQ connection is not available.");
|
||||
|
||||
channel.ExchangeDeclare(exchange: _exchangeName, type: ExchangeType.Direct);
|
||||
|
||||
var body = JsonSerializer.SerializeToUtf8Bytes(@event, @event.GetType(), _eventBusOptions.JsonSerializerOptions);
|
||||
|
||||
return _pipeline.Execute(() =>
|
||||
{
|
||||
IBasicProperties properties = channel.CreateBasicProperties();
|
||||
properties.DeliveryMode = 2;
|
||||
channel.BasicPublish(exchange: _exchangeName, routingKey: routingKey, mandatory: true, basicProperties: properties, body: body);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
private async Task OnMessageReceivedAsync(object sender, BasicDeliverEventArgs eventArgs)
|
||||
{
|
||||
string eventName = eventArgs.RoutingKey;
|
||||
string message = Encoding.UTF8.GetString(eventArgs.Body.Span);
|
||||
|
||||
if (!_eventBusOptions.EventTypes.TryGetValue(eventName, out var eventType))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await using var scope = serviceProvider.CreateAsyncScope();
|
||||
|
||||
var distributedEvent = JsonSerializer.Deserialize(message, eventType, _eventBusOptions.JsonSerializerOptions) as DistributedEvent;
|
||||
|
||||
foreach (var handler in scope.ServiceProvider.GetKeyedServices<IDistributedEventHandler>(eventType))
|
||||
{
|
||||
if (distributedEvent is not null)
|
||||
{
|
||||
await handler.HandleAsync(distributedEvent);
|
||||
}
|
||||
}
|
||||
|
||||
_consumerChannel?.BasicAck(eventArgs.DeliveryTag, multiple: false);
|
||||
}
|
||||
|
||||
private static ResiliencePipeline CreateResiliencePipeline(int retryCount)
|
||||
{
|
||||
var retryOptions = new RetryStrategyOptions
|
||||
{
|
||||
ShouldHandle = new PredicateBuilder().Handle<BrokerUnreachableException>().Handle<SocketException>(),
|
||||
MaxRetryAttempts = retryCount,
|
||||
DelayGenerator = (context) => ValueTask.FromResult(GenerateDelay(context.AttemptNumber))
|
||||
};
|
||||
|
||||
return new ResiliencePipelineBuilder().AddRetry(retryOptions).Build();
|
||||
|
||||
static TimeSpan? GenerateDelay(int attempt) => TimeSpan.FromSeconds(Math.Pow(2, attempt));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) HelloShop Corporation. All rights reserved.
|
||||
// See the license file in the project root for more information.
|
||||
|
||||
using HelloShop.EventBus.Abstractions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace HelloShop.EventBus.RabbitMQ
|
||||
{
|
||||
public static class RabbitMQEventBusExtensions
|
||||
{
|
||||
private const string DefaultSectionName = "RabbitMQEventBus";
|
||||
|
||||
public static IEventBusBuilder AddRabbitMqEventBus(this IHostApplicationBuilder builder, string connectionName, string sectionName = DefaultSectionName)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
|
||||
builder.AddRabbitMQClient(connectionName, configureConnectionFactory: factory =>
|
||||
{
|
||||
factory.DispatchConsumersAsync = true;
|
||||
});
|
||||
|
||||
builder.Services.Configure<EventBusOptions>(builder.Configuration.GetSection(sectionName));
|
||||
|
||||
builder.Services.AddSingleton<IEventBus, RabbitMQEventBus>();
|
||||
builder.Services.AddSingleton<IHostedService>(sp => (RabbitMQEventBus)sp.GetRequiredService<IEventBus>());
|
||||
|
||||
return new EventBusBuilder(builder.Services);
|
||||
}
|
||||
|
||||
private class EventBusBuilder(IServiceCollection services) : IEventBusBuilder
|
||||
{
|
||||
public IServiceCollection Services => services;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) HelloShop Corporation. All rights reserved.
|
||||
// See the license file in the project root for more information.
|
||||
|
||||
namespace HelloShop.EventBus.RabbitMQ
|
||||
{
|
||||
public class RabbitMQEventBusOptions
|
||||
{
|
||||
public string ExchangeName { get; set; } = "event-bus-exchange";
|
||||
|
||||
public required string QueueName { get; set; }
|
||||
|
||||
public int RetryCount { get; set; } = 10;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user