重构代码并抽取基础构建库

This commit is contained in:
administrator committed 2025-03-20 22:13:45 +08:00
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;
}
}