Фиксы
This commit is contained in:
@@ -7,9 +7,8 @@ using FluentAssertions;
|
|||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Knot.Shared.Kernel;
|
using Knot.Shared.Kernel;
|
||||||
using Knot.Modules.Conversations.Domain;
|
using Knot.Contracts.Conversations.Domain;
|
||||||
using Knot.Modules.Messaging.Domain;
|
using Knot.Contracts.Conversations.Application.Abstractions;
|
||||||
using Knot.Modules.Conversations.Application.Abstractions;
|
|
||||||
using Knot.Modules.Conversations.Application.Chats.Create;
|
using Knot.Modules.Conversations.Application.Chats.Create;
|
||||||
|
|
||||||
namespace Knot.Modules.Conversations.UnitTests.Chats;
|
namespace Knot.Modules.Conversations.UnitTests.Chats;
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ using FluentAssertions;
|
|||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Knot.Shared.Kernel;
|
using Knot.Shared.Kernel;
|
||||||
using Knot.Modules.Conversations.Domain;
|
using Knot.Contracts.Conversations.Domain;
|
||||||
using Knot.Modules.Messaging.Domain;
|
using Knot.Contracts.Conversations.Application.Abstractions;
|
||||||
using Knot.Modules.Conversations.Application.Abstractions;
|
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||||
|
using Knot.Contracts.Messaging.Domain;
|
||||||
using Knot.Modules.Conversations.Application.Chats.GetChats;
|
using Knot.Modules.Conversations.Application.Chats.GetChats;
|
||||||
using Knot.Modules.Conversations.Application.DTOs;
|
using Knot.Modules.Conversations.Application.DTOs;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Knot.Modules.Conversations.Application.Abstractions;
|
using Knot.Contracts.Conversations.Application.Abstractions;
|
||||||
using Knot.Modules.Conversations.Application.Chats.GetOrCreateFavorites;
|
using Knot.Modules.Conversations.Application.Chats.GetOrCreateFavorites;
|
||||||
using Knot.Modules.Conversations.Domain;
|
using Knot.Contracts.Conversations.Domain;
|
||||||
using Knot.Modules.Messaging.Domain;
|
|
||||||
using Knot.Shared.Kernel;
|
using Knot.Shared.Kernel;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\..\src\Modules\Chats\Knot.Modules.Conversations.csproj" />
|
<ProjectReference Include="..\..\..\src\Contracts\Conversations\Knot.Contracts.Conversations.csproj" />
|
||||||
<ProjectReference Include="..\..\..\src\Modules\Messaging\Knot.Modules.Messaging.csproj" />
|
<ProjectReference Include="..\..\..\src\Modules\Messaging\Knot.Modules.Messaging.csproj" />
|
||||||
<ProjectReference Include="..\..\..\src\Modules\Conversations\Knot.Modules.Conversations.csproj" />
|
<ProjectReference Include="..\..\..\src\Modules\Conversations\Knot.Modules.Conversations.csproj" />
|
||||||
<ProjectReference Include="..\..\..\src\Modules\Settings\Knot.Modules.Settings.csproj" />
|
<ProjectReference Include="..\..\..\src\Modules\Settings\Knot.Modules.Settings.csproj" />
|
||||||
|
|||||||
@@ -5,15 +5,17 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
using Knot.Contracts.Conversations.Application.Abstractions;
|
||||||
|
using Knot.Contracts.Conversations.Domain;
|
||||||
|
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||||
|
using Knot.Contracts.Messaging.Domain;
|
||||||
using Knot.Shared.Kernel;
|
using Knot.Shared.Kernel;
|
||||||
using Knot.Modules.Conversations.Domain;
|
|
||||||
using Knot.Modules.Messaging.Domain;
|
|
||||||
using Knot.Modules.Conversations.Application.Abstractions;
|
|
||||||
using Knot.Modules.Conversations.Application.Messages.Send;
|
using Knot.Modules.Conversations.Application.Messages.Send;
|
||||||
using Knot.Modules.Settings.Application.Settings.Abstractions;
|
using Knot.Contracts.Settings.Application.Abstractions;
|
||||||
using Knot.Modules.Settings.Application.Settings.DTOs;
|
using Knot.Contracts.Settings.Application.DTOs;
|
||||||
|
|
||||||
namespace Knot.Modules.Conversations.UnitTests.Messages;
|
namespace Knot.Modules.Conversations.UnitTests.Messages;
|
||||||
|
|
||||||
@@ -24,6 +26,8 @@ public class SendMessageCommandHandlerTests
|
|||||||
private readonly IChatsUnitOfWork _unitOfWork;
|
private readonly IChatsUnitOfWork _unitOfWork;
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
private readonly IMessagesSettings _messagesSettings;
|
private readonly IMessagesSettings _messagesSettings;
|
||||||
|
private readonly IIdempotencyStore _idempotencyStore;
|
||||||
|
private readonly ILogger<SendMessageCommandHandler> _logger;
|
||||||
private readonly SendMessageCommandHandler _handler;
|
private readonly SendMessageCommandHandler _handler;
|
||||||
|
|
||||||
public SendMessageCommandHandlerTests()
|
public SendMessageCommandHandlerTests()
|
||||||
@@ -33,11 +37,13 @@ public class SendMessageCommandHandlerTests
|
|||||||
_unitOfWork = Substitute.For<IChatsUnitOfWork>();
|
_unitOfWork = Substitute.For<IChatsUnitOfWork>();
|
||||||
_mediator = Substitute.For<IMediator>();
|
_mediator = Substitute.For<IMediator>();
|
||||||
_messagesSettings = Substitute.For<IMessagesSettings>();
|
_messagesSettings = Substitute.For<IMessagesSettings>();
|
||||||
|
_idempotencyStore = Substitute.For<IIdempotencyStore>();
|
||||||
|
_logger = Substitute.For<ILogger<SendMessageCommandHandler>>();
|
||||||
|
|
||||||
var config = new Knot.Modules.Settings.Application.Settings.DTOs.MessagesConfig();
|
var config = new MessagesConfig();
|
||||||
_messagesSettings.Current.Returns(config);
|
_messagesSettings.Current.Returns(config);
|
||||||
|
|
||||||
_handler = new SendMessageCommandHandler(_chatRepository, _messageRepository, _unitOfWork, _mediator, _messagesSettings);
|
_handler = new SendMessageCommandHandler(_chatRepository, _messageRepository, _unitOfWork, _mediator, _messagesSettings, _idempotencyStore, _logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace Knot.Contracts.Conversations.Application.Abstractions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Хранилище для обеспечения идемпотентности операций.
|
||||||
|
/// Если ключ уже существует — возвращает сохранённый результат без повторного выполнения.
|
||||||
|
/// </summary>
|
||||||
|
public interface IIdempotencyStore
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Возвращает сохранённый результат по ключу или выполняет factory, сохраняет и возвращает результат.
|
||||||
|
/// </summary>
|
||||||
|
Task<T> GetOrCreateAsync<T>(
|
||||||
|
string key,
|
||||||
|
Func<CancellationToken, Task<T>> factory,
|
||||||
|
TimeSpan? expiration = null,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -24,7 +24,6 @@
|
|||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.5" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.5" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.16.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.16.0" />
|
||||||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.16.0" />
|
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.16.0" />
|
||||||
<PackageReference Include="DKNet.AspCore.Idempotency" Version="1.0.0" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ using Knot.Modules.Conversations;
|
|||||||
using Knot.Modules.Conversations.Infrastructure.Persistence;
|
using Knot.Modules.Conversations.Infrastructure.Persistence;
|
||||||
using Knot.Modules.Conversations.Infrastructure.SignalR;
|
using Knot.Modules.Conversations.Infrastructure.SignalR;
|
||||||
using Knot.Modules.Conversations.Presentation.Endpoints;
|
using Knot.Modules.Conversations.Presentation.Endpoints;
|
||||||
using Knot.Modules.Conversations.Presentation.Middleware;
|
|
||||||
using Knot.Modules.Federation;
|
using Knot.Modules.Federation;
|
||||||
using Knot.Modules.Federation.Presentation.Endpoints;
|
using Knot.Modules.Federation.Presentation.Endpoints;
|
||||||
using Knot.Modules.Klipy;
|
using Knot.Modules.Klipy;
|
||||||
@@ -141,7 +140,6 @@ builder.Services.AddRouting(options =>
|
|||||||
|
|
||||||
builder.Services.AddMemoryCache();
|
builder.Services.AddMemoryCache();
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
builder.Services.AddIdempotency(); // DKNet.AspCore.Idempotency DI registration
|
|
||||||
|
|
||||||
builder.Services.ConfigureHttpJsonOptions(options =>
|
builder.Services.ConfigureHttpJsonOptions(options =>
|
||||||
{
|
{
|
||||||
@@ -244,7 +242,6 @@ if (app.Environment.IsDevelopment())
|
|||||||
// Не раздаем статические файлы, так как теперь используем MinIO
|
// Не раздаем статические файлы, так как теперь используем MinIO
|
||||||
|
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseIdempotency(); // DKNet.AspCore.Idempotency middleware
|
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
// Регистрация эндпоинтов
|
// Регистрация эндпоинтов
|
||||||
|
|||||||
+20
-34
@@ -41,6 +41,7 @@ public sealed class SendMessageCommandHandler : ICommandHandler<SendMessageComma
|
|||||||
private readonly IChatsUnitOfWork _unitOfWork;
|
private readonly IChatsUnitOfWork _unitOfWork;
|
||||||
private readonly MediatR.IMediator _mediator;
|
private readonly MediatR.IMediator _mediator;
|
||||||
private readonly IMessagesSettings _messagesSettings;
|
private readonly IMessagesSettings _messagesSettings;
|
||||||
|
private readonly IIdempotencyStore _idempotencyStore;
|
||||||
private readonly ILogger<SendMessageCommandHandler> _logger;
|
private readonly ILogger<SendMessageCommandHandler> _logger;
|
||||||
|
|
||||||
public SendMessageCommandHandler(
|
public SendMessageCommandHandler(
|
||||||
@@ -49,6 +50,7 @@ public sealed class SendMessageCommandHandler : ICommandHandler<SendMessageComma
|
|||||||
IChatsUnitOfWork unitOfWork,
|
IChatsUnitOfWork unitOfWork,
|
||||||
MediatR.IMediator mediator,
|
MediatR.IMediator mediator,
|
||||||
IMessagesSettings messagesSettings,
|
IMessagesSettings messagesSettings,
|
||||||
|
IIdempotencyStore idempotencyStore,
|
||||||
ILogger<SendMessageCommandHandler> logger)
|
ILogger<SendMessageCommandHandler> logger)
|
||||||
{
|
{
|
||||||
_chatRepository = chatRepository;
|
_chatRepository = chatRepository;
|
||||||
@@ -56,10 +58,25 @@ public sealed class SendMessageCommandHandler : ICommandHandler<SendMessageComma
|
|||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
_messagesSettings = messagesSettings;
|
_messagesSettings = messagesSettings;
|
||||||
|
_idempotencyStore = idempotencyStore;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<Guid>> Handle(SendMessageCommand request, CancellationToken cancellationToken)
|
public async Task<Result<Guid>> Handle(SendMessageCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(request.IdempotencyKey))
|
||||||
|
{
|
||||||
|
var key = $"send_msg:{request.ChatId}:{request.IdempotencyKey}";
|
||||||
|
return await _idempotencyStore.GetOrCreateAsync(
|
||||||
|
key,
|
||||||
|
factory: ct => ExecuteAsync(request, ct),
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await ExecuteAsync(request, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Result<Guid>> ExecuteAsync(SendMessageCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// 1. Проверка существования чата
|
// 1. Проверка существования чата
|
||||||
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
|
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
|
||||||
@@ -68,40 +85,30 @@ public sealed class SendMessageCommandHandler : ICommandHandler<SendMessageComma
|
|||||||
return Result.Failure<Guid>(ChatErrors.ChatsNotFound);
|
return Result.Failure<Guid>(ChatErrors.ChatsNotFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. ���������, �������� �� ����������� ����������
|
// 2. Проверка, является ли отправитель участником чата
|
||||||
if (!chat.Members.Any(m => m.UserId == request.SenderId))
|
if (!chat.Members.Any(m => m.UserId == request.SenderId))
|
||||||
{
|
{
|
||||||
return Result.Failure<Guid>(ChatErrors.ChatsForbidden);
|
return Result.Failure<Guid>(ChatErrors.ChatsForbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. ������� ���������
|
// 3. Создание сообщения
|
||||||
Message message;
|
Message message;
|
||||||
if (request.Type == "story_reply" || request.Type == "story_reaction")
|
if (request.Type == "story_reply" || request.Type == "story_reaction")
|
||||||
{
|
{
|
||||||
if (!_messagesSettings.Current.AllowMedia) return Result.Failure<Guid>(ChatErrors.MediaDisabled);
|
if (!_messagesSettings.Current.AllowMedia) return Result.Failure<Guid>(ChatErrors.MediaDisabled);
|
||||||
|
|
||||||
|
|
||||||
var parsedStoryMediaType = Enum.TryParse<MediaType>(request.StoryMediaType, true, out var sTypeEnum) ? sTypeEnum : MediaType.Image;
|
var parsedStoryMediaType = Enum.TryParse<MediaType>(request.StoryMediaType, true, out var sTypeEnum) ? sTypeEnum : MediaType.Image;
|
||||||
message = new StoryMessage(
|
message = new StoryMessage(
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
|
|
||||||
request.ChatId,
|
request.ChatId,
|
||||||
|
|
||||||
request.SenderId,
|
request.SenderId,
|
||||||
|
|
||||||
request.StoryId ?? Guid.Empty,
|
request.StoryId ?? Guid.Empty,
|
||||||
|
|
||||||
request.StoryMediaUrl ?? string.Empty,
|
request.StoryMediaUrl ?? string.Empty,
|
||||||
request.StoryMediaType,
|
request.StoryMediaType,
|
||||||
|
|
||||||
request.Content,
|
request.Content,
|
||||||
|
|
||||||
request.ReplyToId,
|
request.ReplyToId,
|
||||||
|
|
||||||
request.ForwardedFromId,
|
request.ForwardedFromId,
|
||||||
|
|
||||||
DateTime.UtcNow,
|
DateTime.UtcNow,
|
||||||
|
|
||||||
false);
|
false);
|
||||||
}
|
}
|
||||||
else if (request.Attachments != null && request.Attachments.Any())
|
else if (request.Attachments != null && request.Attachments.Any())
|
||||||
@@ -111,26 +118,17 @@ public sealed class SendMessageCommandHandler : ICommandHandler<SendMessageComma
|
|||||||
var firstAtt = request.Attachments.First();
|
var firstAtt = request.Attachments.First();
|
||||||
var parsedType = Enum.TryParse<MediaType>(firstAtt.Type, true, out var mTypeEnum) ? mTypeEnum : MediaType.File;
|
var parsedType = Enum.TryParse<MediaType>(firstAtt.Type, true, out var mTypeEnum) ? mTypeEnum : MediaType.File;
|
||||||
|
|
||||||
|
|
||||||
message = new MediaMessage(
|
message = new MediaMessage(
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
|
|
||||||
request.ChatId,
|
request.ChatId,
|
||||||
|
|
||||||
request.SenderId,
|
request.SenderId,
|
||||||
parsedType,
|
parsedType,
|
||||||
|
|
||||||
request.Content,
|
request.Content,
|
||||||
|
|
||||||
request.ReplyToId,
|
request.ReplyToId,
|
||||||
|
|
||||||
request.ForwardedFromId,
|
request.ForwardedFromId,
|
||||||
|
|
||||||
DateTime.UtcNow,
|
DateTime.UtcNow,
|
||||||
|
|
||||||
false);
|
false);
|
||||||
|
|
||||||
|
|
||||||
foreach (var att in request.Attachments)
|
foreach (var att in request.Attachments)
|
||||||
{
|
{
|
||||||
var pType = Enum.TryParse<MediaType>(att.Type, true, out var tEnum) ? tEnum : MediaType.File;
|
var pType = Enum.TryParse<MediaType>(att.Type, true, out var tEnum) ? tEnum : MediaType.File;
|
||||||
@@ -172,25 +170,17 @@ public sealed class SendMessageCommandHandler : ICommandHandler<SendMessageComma
|
|||||||
{
|
{
|
||||||
message = new TextMessage(
|
message = new TextMessage(
|
||||||
Guid.NewGuid(),
|
Guid.NewGuid(),
|
||||||
|
|
||||||
request.ChatId,
|
request.ChatId,
|
||||||
|
|
||||||
request.SenderId,
|
request.SenderId,
|
||||||
|
|
||||||
request.Content ?? string.Empty,
|
request.Content ?? string.Empty,
|
||||||
|
|
||||||
request.ReplyToId,
|
request.ReplyToId,
|
||||||
|
|
||||||
request.Quote,
|
request.Quote,
|
||||||
|
|
||||||
request.ForwardedFromId,
|
request.ForwardedFromId,
|
||||||
|
|
||||||
DateTime.UtcNow,
|
DateTime.UtcNow,
|
||||||
|
|
||||||
false);
|
false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. ������������������ ��������� High-Water Mark
|
// 4. Обновление High-Water Mark
|
||||||
chat.IncrementSequenceId();
|
chat.IncrementSequenceId();
|
||||||
message.SetSequenceId(chat.LastMessageSequenceId);
|
message.SetSequenceId(chat.LastMessageSequenceId);
|
||||||
|
|
||||||
@@ -204,13 +194,9 @@ public sealed class SendMessageCommandHandler : ICommandHandler<SendMessageComma
|
|||||||
|
|
||||||
await _mediator.Publish(new MessageSentDomainEvent(
|
await _mediator.Publish(new MessageSentDomainEvent(
|
||||||
message.Id,
|
message.Id,
|
||||||
|
|
||||||
message.ChatId,
|
message.ChatId,
|
||||||
|
|
||||||
message.SenderId,
|
message.SenderId,
|
||||||
|
|
||||||
message.Content),
|
message.Content),
|
||||||
|
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
return Result.Success(message.Id);
|
return Result.Success(message.Id);
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ using Knot.Contracts.Conversations.Application.Abstractions;
|
|||||||
using Knot.Contracts.Conversations.Domain;
|
using Knot.Contracts.Conversations.Domain;
|
||||||
using Knot.Contracts.Conversations.Infrastructure.Persistence;
|
using Knot.Contracts.Conversations.Infrastructure.Persistence;
|
||||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||||
|
using Knot.Modules.Conversations.Infrastructure.Idempotency;
|
||||||
using Knot.Modules.Conversations.Infrastructure.Persistence;
|
using Knot.Modules.Conversations.Infrastructure.Persistence;
|
||||||
|
using Knot.Modules.Conversations.Infrastructure.Persistence.Mongo;
|
||||||
using Knot.Modules.Conversations.Infrastructure.Services;
|
using Knot.Modules.Conversations.Infrastructure.Services;
|
||||||
using Knot.Shared.Kernel;
|
using Knot.Shared.Kernel;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -42,6 +44,9 @@ public static class DependencyInjection
|
|||||||
services.AddScoped<Knot.Contracts.Conversations.Abstractions.IUserStatusService, UserStatusService>();
|
services.AddScoped<Knot.Contracts.Conversations.Abstractions.IUserStatusService, UserStatusService>();
|
||||||
services.AddScoped<Knot.Contracts.Conversations.Abstractions.IUserDeleterService, UserDeleterService>();
|
services.AddScoped<Knot.Contracts.Conversations.Abstractions.IUserDeleterService, UserDeleterService>();
|
||||||
|
|
||||||
|
services.AddMemoryCache();
|
||||||
|
services.AddSingleton<Knot.Contracts.Conversations.Application.Abstractions.IIdempotencyStore, MemoryCacheIdempotencyStore>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
using Knot.Contracts.Conversations.Application.Abstractions;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
|
||||||
|
namespace Knot.Modules.Conversations.Infrastructure.Idempotency;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Реализация хранилища идемпотентности на основе IMemoryCache.
|
||||||
|
/// Использует семафор для предотвращения race condition при одновременных запросах с одинаковым ключом.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MemoryCacheIdempotencyStore : IIdempotencyStore
|
||||||
|
{
|
||||||
|
private readonly IMemoryCache _cache;
|
||||||
|
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||||
|
|
||||||
|
public MemoryCacheIdempotencyStore(IMemoryCache cache)
|
||||||
|
{
|
||||||
|
_cache = cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<T> GetOrCreateAsync<T>(
|
||||||
|
string key,
|
||||||
|
Func<CancellationToken, Task<T>> factory,
|
||||||
|
TimeSpan? expiration = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (_cache.TryGetValue(key, out T? cachedValue) && cachedValue is not null)
|
||||||
|
{
|
||||||
|
return cachedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _semaphore.WaitAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Double-check после получения блокировки
|
||||||
|
if (_cache.TryGetValue(key, out cachedValue) && cachedValue is not null)
|
||||||
|
{
|
||||||
|
return cachedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = await factory(cancellationToken);
|
||||||
|
|
||||||
|
var options = new MemoryCacheEntryOptions()
|
||||||
|
.SetAbsoluteExpiration(expiration ?? TimeSpan.FromHours(24))
|
||||||
|
.SetPriority(CacheItemPriority.Normal);
|
||||||
|
|
||||||
|
_cache.Set(key, value, options);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,7 +29,6 @@
|
|||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
||||||
<PackageReference Include="MongoDB.Driver" Version="3.2.0" />
|
<PackageReference Include="MongoDB.Driver" Version="3.2.0" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||||
<PackageReference Include="DKNet.AspCore.Idempotency" Version="1.0.0" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -38,6 +37,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
|
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
|
||||||
|
<InternalsVisibleTo Include="Knot.Modules.Conversations.UnitTests" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Reference in New Issue
Block a user