net9-update-refactorings #1

Merged
michael merged 17 commits from net9-update-refactorings into master 2025-01-30 06:33:51 +00:00
11 changed files with 503 additions and 12 deletions
Showing only changes of commit f59a6da2da - Show all commits

View File

@ -16,10 +16,10 @@ namespace Provider
public RedisStreamAdapter(IDatabase database, string providerName, HashRingBasedStreamQueueMapper hashRingBasedStreamQueueMapper, ILoggerFactory loggerFactory) public RedisStreamAdapter(IDatabase database, string providerName, HashRingBasedStreamQueueMapper hashRingBasedStreamQueueMapper, ILoggerFactory loggerFactory)
{ {
_database = database; _database = database ?? throw new ArgumentNullException(nameof(database));
_providerName = providerName; _providerName = providerName ?? throw new ArgumentNullException(nameof(providerName));
_hashRingBasedStreamQueueMapper = hashRingBasedStreamQueueMapper; _hashRingBasedStreamQueueMapper = hashRingBasedStreamQueueMapper ?? throw new ArgumentNullException(nameof(hashRingBasedStreamQueueMapper));
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
_logger = loggerFactory.CreateLogger<RedisStreamAdapter>(); _logger = loggerFactory.CreateLogger<RedisStreamAdapter>();
} }

View File

@ -24,11 +24,16 @@ namespace Provider
HashRingStreamQueueMapperOptions hashRingStreamQueueMapperOptions HashRingStreamQueueMapperOptions hashRingStreamQueueMapperOptions
) )
{ {
_connectionMultiplexer = connectionMultiplexer; if (hashRingStreamQueueMapperOptions is null)
_loggerFactory = loggerFactory; {
_providerName = providerName; throw new ArgumentNullException(nameof(hashRingStreamQueueMapperOptions));
_streamFailureHandler = streamFailureHandler; }
_simpleQueueCacheOptions = simpleQueueCacheOptions;
_connectionMultiplexer = connectionMultiplexer ?? throw new ArgumentNullException(nameof(connectionMultiplexer));
_loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
_providerName = providerName ?? throw new ArgumentNullException(nameof(providerName));
_streamFailureHandler = streamFailureHandler ?? throw new ArgumentNullException(nameof(streamFailureHandler));
_simpleQueueCacheOptions = simpleQueueCacheOptions ?? throw new ArgumentNullException(nameof(simpleQueueCacheOptions));
_hashRingBasedStreamQueueMapper = new HashRingBasedStreamQueueMapper(hashRingStreamQueueMapperOptions, providerName); _hashRingBasedStreamQueueMapper = new HashRingBasedStreamQueueMapper(hashRingStreamQueueMapperOptions, providerName);
} }

View File

@ -10,7 +10,7 @@ namespace Provider
public RedisStreamFailureHandler(ILogger<RedisStreamFailureHandler> logger) public RedisStreamFailureHandler(ILogger<RedisStreamFailureHandler> logger)
{ {
_logger = logger; _logger = logger ?? throw new ArgumentNullException(nameof(logger));
} }
public bool ShouldFaultSubsriptionOnError => true; public bool ShouldFaultSubsriptionOnError => true;

View File

@ -16,8 +16,8 @@ namespace Provider
public RedisStreamReceiver(QueueId queueId, IDatabase database, ILogger<RedisStreamReceiver> logger) public RedisStreamReceiver(QueueId queueId, IDatabase database, ILogger<RedisStreamReceiver> logger)
{ {
_queueId = queueId; _queueId = queueId;
_database = database; _database = database ?? throw new ArgumentNullException(nameof(database));
_logger = logger; _logger = logger ?? throw new ArgumentNullException(nameof(logger));
} }
public async Task<IList<IBatchContainer>?> GetQueueMessagesAsync(int maxCount) public async Task<IList<IBatchContainer>?> GetQueueMessagesAsync(int maxCount)

View File

@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client", "Client\Client.csp
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Provider", "Provider\Provider.csproj", "{70F8E685-F662-4225-A60C-D318E0C6ED18}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Provider", "Provider\Provider.csproj", "{70F8E685-F662-4225-A60C-D318E0C6ED18}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedisStreamsProvider.UnitTests", "RedisStreamsProvider.UnitTests\RedisStreamsProvider.UnitTests.csproj", "{DF927C2B-A141-4476-86CF-3B4DC8ECB4DE}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -27,6 +29,10 @@ Global
{70F8E685-F662-4225-A60C-D318E0C6ED18}.Debug|Any CPU.Build.0 = Debug|Any CPU {70F8E685-F662-4225-A60C-D318E0C6ED18}.Debug|Any CPU.Build.0 = Debug|Any CPU
{70F8E685-F662-4225-A60C-D318E0C6ED18}.Release|Any CPU.ActiveCfg = Release|Any CPU {70F8E685-F662-4225-A60C-D318E0C6ED18}.Release|Any CPU.ActiveCfg = Release|Any CPU
{70F8E685-F662-4225-A60C-D318E0C6ED18}.Release|Any CPU.Build.0 = Release|Any CPU {70F8E685-F662-4225-A60C-D318E0C6ED18}.Release|Any CPU.Build.0 = Release|Any CPU
{DF927C2B-A141-4476-86CF-3B4DC8ECB4DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DF927C2B-A141-4476-86CF-3B4DC8ECB4DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DF927C2B-A141-4476-86CF-3B4DC8ECB4DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DF927C2B-A141-4476-86CF-3B4DC8ECB4DE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@ -0,0 +1,48 @@
using Moq;
using StackExchange.Redis;
using Microsoft.Extensions.Logging;
using Orleans.Streams;
using Xunit;
using Provider;
using System.Collections.Generic;
using System.Threading.Tasks;
using Orleans.Configuration;
namespace RedisStreamsProvider.UnitTests
{
public class RedisStreamAdapterTests
{
private readonly Mock<IDatabase> _mockDatabase;
private readonly Mock<HashRingBasedStreamQueueMapper> _mockQueueMapper;
private readonly Mock<ILoggerFactory> _mockLoggerFactory;
private readonly RedisStreamAdapter _adapter;
public RedisStreamAdapterTests()
{
_mockDatabase = new Mock<IDatabase>();
var options = new HashRingStreamQueueMapperOptions { TotalQueueCount = 1 };
_mockQueueMapper = new Mock<HashRingBasedStreamQueueMapper>(options, "queueNamePrefix");
_mockLoggerFactory = new Mock<ILoggerFactory>();
_adapter = new RedisStreamAdapter(_mockDatabase.Object, "TestProvider", _mockQueueMapper.Object, _mockLoggerFactory.Object);
}
[Fact]
public void Constructor_ShouldInitializeProperties()
{
Assert.Equal("TestProvider", _adapter.Name);
Assert.False(_adapter.IsRewindable);
Assert.Equal(StreamProviderDirection.ReadWrite, _adapter.Direction);
}
[Fact]
public void CreateReceiver_ShouldReturnRedisStreamReceiver()
{
var queueId = QueueId.GetQueueId("queueName", 0, 1);
var receiver = _adapter.CreateReceiver(queueId);
Assert.NotNull(receiver);
Assert.IsType<RedisStreamReceiver>(receiver);
}
}
}

View File

@ -0,0 +1,80 @@
using Provider;
using StackExchange.Redis;
using System.Text.Json;
using Xunit;
namespace RedisStreamsProvider.UnitTests
{
public class RedisStreamBatchContainerTests
{
[Fact]
public void Constructor_ShouldInitializeProperties()
{
// Arrange
var streamEntry = new StreamEntry("1-0", new NameValueEntry[]
{
new NameValueEntry("namespace", "testNamespace"),
new NameValueEntry("key", "testKey"),
new NameValueEntry("type", "TestEvent"),
new NameValueEntry("data", JsonSerializer.Serialize(new TestEvent { Id = 1, Name = "Test" }))
});
// Act
var container = new RedisStreamBatchContainer(streamEntry);
// Assert
Assert.Equal("testNamespace", container.StreamId.GetNamespace());
Assert.Equal("testKey", container.StreamId.GetKeyAsString());
Assert.Equal(streamEntry.Id.ToString().Split('-').First(), container.SequenceToken.SequenceNumber.ToString());
}
[Fact]
public void GetEvents_ShouldReturnDeserializedEvents()
{
// Arrange
var streamEntry = new StreamEntry("1-0", new NameValueEntry[]
{
new NameValueEntry("namespace", "testNamespace"),
new NameValueEntry("key", "testKey"),
new NameValueEntry("type", "TestEvent"),
new NameValueEntry("data", JsonSerializer.Serialize(new TestEvent { Id = 1, Name = "Test" }))
});
var container = new RedisStreamBatchContainer(streamEntry);
// Act
var events = container.GetEvents<TestEvent>().ToList();
// Assert
Assert.Single(events);
Assert.Equal(1, events[0].Item1.Id);
Assert.Equal("Test", events[0].Item1.Name);
Assert.Equal(streamEntry.Id.ToString().Split('-').First(), events[0].Item2.SequenceNumber.ToString());
}
[Fact]
public void ImportRequestContext_ShouldReturnFalse()
{
// Arrange
var streamEntry = new StreamEntry("1-0", new NameValueEntry[]
{
new NameValueEntry("namespace", "testNamespace"),
new NameValueEntry("key", "testKey"),
new NameValueEntry("type", "TestEvent"),
new NameValueEntry("data", JsonSerializer.Serialize(new TestEvent { Id = 1, Name = "Test" }))
});
var container = new RedisStreamBatchContainer(streamEntry);
// Act
var result = container.ImportRequestContext();
// Assert
Assert.False(result);
}
private class TestEvent
{
public int Id { get; set; }
public string Name { get; set; }
}
}
}

View File

@ -0,0 +1,90 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Moq;
using Orleans.Configuration;
using Orleans.Providers.Streams.Common;
using Orleans.Streams;
using Provider;
using StackExchange.Redis;
using Xunit;
namespace RedisStreamsProvider.UnitTests
{
public class RedisStreamFactoryTests
{
private readonly Mock<IConnectionMultiplexer> _mockConnectionMultiplexer;
private readonly Mock<ILoggerFactory> _mockLoggerFactory;
private readonly Mock<IServiceProvider> _mockServiceProvider;
private readonly Mock<IStreamFailureHandler> _mockStreamFailureHandler;
private readonly SimpleQueueCacheOptions _simpleQueueCacheOptions;
private readonly HashRingStreamQueueMapperOptions _hashRingStreamQueueMapperOptions;
private readonly string _providerName = "TestProvider";
public RedisStreamFactoryTests()
{
_mockConnectionMultiplexer = new Mock<IConnectionMultiplexer>();
_mockConnectionMultiplexer.Setup(x => x.GetDatabase(It.IsAny<int>(), It.IsAny<object>())).Returns(new Mock<IDatabase>().Object);
_mockLoggerFactory = new Mock<ILoggerFactory>();
_mockServiceProvider = new Mock<IServiceProvider>();
_mockStreamFailureHandler = new Mock<IStreamFailureHandler>();
_simpleQueueCacheOptions = new SimpleQueueCacheOptions();
_hashRingStreamQueueMapperOptions = new HashRingStreamQueueMapperOptions();
}
[Fact]
public void Constructor_ShouldThrowArgumentNullException_WhenAnyArgumentIsNull()
{
Assert.Throws<ArgumentNullException>(() => new RedisStreamFactory(null, _mockLoggerFactory.Object, _providerName, _mockStreamFailureHandler.Object, _simpleQueueCacheOptions, _hashRingStreamQueueMapperOptions));
Assert.Throws<ArgumentNullException>(() => new RedisStreamFactory(_mockConnectionMultiplexer.Object, null, _providerName, _mockStreamFailureHandler.Object, _simpleQueueCacheOptions, _hashRingStreamQueueMapperOptions));
Assert.Throws<ArgumentNullException>(() => new RedisStreamFactory(_mockConnectionMultiplexer.Object, _mockLoggerFactory.Object, null, _mockStreamFailureHandler.Object, _simpleQueueCacheOptions, _hashRingStreamQueueMapperOptions));
Assert.Throws<ArgumentNullException>(() => new RedisStreamFactory(_mockConnectionMultiplexer.Object, _mockLoggerFactory.Object, _providerName, null, _simpleQueueCacheOptions, _hashRingStreamQueueMapperOptions));
Assert.Throws<ArgumentNullException>(() => new RedisStreamFactory(_mockConnectionMultiplexer.Object, _mockLoggerFactory.Object, _providerName, _mockStreamFailureHandler.Object, null, _hashRingStreamQueueMapperOptions));
Assert.Throws<ArgumentNullException>(() => new RedisStreamFactory(_mockConnectionMultiplexer.Object, _mockLoggerFactory.Object, _providerName, _mockStreamFailureHandler.Object, _simpleQueueCacheOptions, null));
}
[Fact]
public async Task CreateAdapter_ShouldReturnRedisStreamAdapterInstance()
{
var factory = new RedisStreamFactory(_mockConnectionMultiplexer.Object, _mockLoggerFactory.Object, _providerName, _mockStreamFailureHandler.Object, _simpleQueueCacheOptions, _hashRingStreamQueueMapperOptions);
var adapter = await factory.CreateAdapter();
Assert.NotNull(adapter);
Assert.IsType<RedisStreamAdapter>(adapter);
}
[Fact]
public async Task GetDeliveryFailureHandler_ShouldReturnStreamFailureHandler()
{
var factory = new RedisStreamFactory(_mockConnectionMultiplexer.Object, _mockLoggerFactory.Object, _providerName, _mockStreamFailureHandler.Object, _simpleQueueCacheOptions, _hashRingStreamQueueMapperOptions);
var handler = await factory.GetDeliveryFailureHandler(new QueueId());
Assert.NotNull(handler);
Assert.Equal(_mockStreamFailureHandler.Object, handler);
}
[Fact]
public void GetQueueAdapterCache_ShouldReturnSimpleQueueAdapterCacheInstance()
{
var factory = new RedisStreamFactory(_mockConnectionMultiplexer.Object, _mockLoggerFactory.Object, _providerName, _mockStreamFailureHandler.Object, _simpleQueueCacheOptions, _hashRingStreamQueueMapperOptions);
var cache = factory.GetQueueAdapterCache();
Assert.NotNull(cache);
Assert.IsType<SimpleQueueAdapterCache>(cache);
}
[Fact]
public void GetStreamQueueMapper_ShouldReturnHashRingBasedStreamQueueMapperInstance()
{
var factory = new RedisStreamFactory(_mockConnectionMultiplexer.Object, _mockLoggerFactory.Object, _providerName, _mockStreamFailureHandler.Object, _simpleQueueCacheOptions, _hashRingStreamQueueMapperOptions);
var mapper = factory.GetStreamQueueMapper();
Assert.NotNull(mapper);
Assert.IsType<HashRingBasedStreamQueueMapper>(mapper);
}
}
}

View File

@ -0,0 +1,104 @@
using Microsoft.Extensions.Logging;
using Moq;
using Orleans.Streams;
using Provider;
using StackExchange.Redis;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Xunit;
namespace RedisStreamsProvider.UnitTests
{
public class RedisStreamReceiverTests
{
private readonly Mock<IDatabase> _mockDatabase;
private readonly Mock<ILogger<RedisStreamReceiver>> _mockLogger;
private readonly QueueId _queueId;
private readonly RedisStreamReceiver _receiver;
public RedisStreamReceiverTests()
{
_mockDatabase = new Mock<IDatabase>();
_mockLogger = new Mock<ILogger<RedisStreamReceiver>>();
_queueId = QueueId.GetQueueId("testQueue", 0, 0); // Added the missing 'hash' parameter
_receiver = new RedisStreamReceiver(_queueId, _mockDatabase.Object, _mockLogger.Object);
}
[Fact]
public async Task GetQueueMessagesAsync_ReturnsBatches()
{
// Arrange
var streamEntries = new[]
{
new StreamEntry("1-0", new NameValueEntry[2] {new NameValueEntry("namespace", "testNamespace"),
new NameValueEntry("key", "testKey")}),
new StreamEntry("2-0", new NameValueEntry[2] {new NameValueEntry("namespace", "testNamespace"),
new NameValueEntry("key", "testKey")})
};
_mockDatabase.Setup(db => db.StreamReadGroupAsync(
It.IsAny<RedisKey>(), It.IsAny<RedisValue>(), It.IsAny<RedisValue>(), It.IsAny<RedisValue?>(),
It.IsAny<int?>(), It.IsAny<bool>(), CommandFlags.None))
.ReturnsAsync(streamEntries);
// Act
var result = await _receiver.GetQueueMessagesAsync(10);
// Assert
Assert.NotNull(result);
Assert.Equal(2, result.Count);
}
[Fact]
public async Task Initialize_CreatesConsumerGroup()
{
// Arrange
_mockDatabase.Setup(db => db.StreamCreateConsumerGroupAsync(It.IsAny<RedisKey>(), It.IsAny<RedisValue>(), It.IsAny<RedisValue>(), It.IsAny<bool>(), CommandFlags.None))
.ReturnsAsync(true);
// Act
await _receiver.Initialize(TimeSpan.FromSeconds(5));
// Assert
_mockDatabase.Verify(db => db.StreamCreateConsumerGroupAsync(_queueId.ToString(), "consumer", "$", true, CommandFlags.None), Times.Once);
}
[Fact]
public async Task MessagesDeliveredAsync_AcknowledgesMessages()
{
// Arrange
var messages = new List<IBatchContainer>
{
new RedisStreamBatchContainer(new StreamEntry("1-0", new NameValueEntry[2] {new NameValueEntry("namespace", "testNamespace"),
new NameValueEntry("key", "testKey")})),
new RedisStreamBatchContainer(new StreamEntry("2-0", new NameValueEntry[2] {new NameValueEntry("namespace", "testNamespace"),
new NameValueEntry("key", "testKey")}))
};
_mockDatabase.Setup(db => db.StreamAcknowledgeAsync(It.IsAny<RedisKey>(), It.IsAny<RedisValue>(), It.IsAny<RedisValue>(), CommandFlags.None))
.ReturnsAsync(2);
// Act
await _receiver.MessagesDeliveredAsync(messages);
// Assert
_mockDatabase.Verify(db => db.StreamAcknowledgeAsync(_queueId.ToString(), "consumer", "1-0", CommandFlags.None), Times.Once);
_mockDatabase.Verify(db => db.StreamAcknowledgeAsync(_queueId.ToString(), "consumer", "2-0", CommandFlags.None), Times.Once);
}
[Fact]
public async Task Shutdown_WaitsForPendingTasks()
{
// Arrange
var tcs = new TaskCompletionSource<StreamEntry[]>();
_mockDatabase.Setup(db => db.StreamReadGroupAsync(It.IsAny<RedisKey>(), It.IsAny<RedisValue>(), It.IsAny<RedisValue>(), It.IsAny<RedisValue>(), It.IsAny<int>(), CommandFlags.None))
.Returns(tcs.Task);
// Act
var getMessagesTask = _receiver.GetQueueMessagesAsync(10);
await _receiver.Shutdown(TimeSpan.FromSeconds(5));
// Assert
Assert.True(getMessagesTask.IsCompleted);
}
}
}

View File

@ -0,0 +1,132 @@
using Xunit;
using StackExchange.Redis;
using Provider;
using Orleans.Streams;
using System;
using Moq;
namespace Provider.Tests
{
public class RedisStreamSequenceTokenTests
{
[Fact]
public void Constructor_ShouldInitializeProperties_FromRedisValue()
{
// Arrange
var redisValue = new RedisValue("123-456");
// Act
var token = new RedisStreamSequenceToken(redisValue);
// Assert
Assert.Equal(123, token.SequenceNumber);
Assert.Equal(456, token.EventIndex);
}
[Fact]
public void Constructor_ShouldInitializeProperties_FromParameters()
{
// Arrange
long sequenceNumber = 123;
int eventIndex = 456;
// Act
var token = new RedisStreamSequenceToken(sequenceNumber, eventIndex);
// Assert
Assert.Equal(sequenceNumber, token.SequenceNumber);
Assert.Equal(eventIndex, token.EventIndex);
}
[Fact]
public void CompareTo_ShouldReturnZero_ForEqualTokens()
{
// Arrange
var token1 = new RedisStreamSequenceToken(123, 456);
var token2 = new RedisStreamSequenceToken(123, 456);
// Act
var result = token1.CompareTo(token2);
// Assert
Assert.Equal(0, result);
}
[Fact]
public void CompareTo_ShouldReturnPositive_ForGreaterToken()
{
// Arrange
var token1 = new RedisStreamSequenceToken(123, 456);
var token2 = new RedisStreamSequenceToken(123, 455);
// Act
var result = token1.CompareTo(token2);
// Assert
Assert.True(result > 0);
}
[Fact]
public void CompareTo_ShouldReturnNegative_ForLesserToken()
{
// Arrange
var token1 = new RedisStreamSequenceToken(123, 455);
var token2 = new RedisStreamSequenceToken(123, 456);
// Act
var result = token1.CompareTo(token2);
// Assert
Assert.True(result < 0);
}
[Fact]
public void Equals_ShouldReturnTrue_ForEqualTokens()
{
// Arrange
var token1 = new RedisStreamSequenceToken(123, 456);
var token2 = new RedisStreamSequenceToken(123, 456);
// Act
var result = token1.Equals(token2);
// Assert
Assert.True(result);
}
[Fact]
public void Equals_ShouldReturnFalse_ForDifferentTokens()
{
// Arrange
var token1 = new RedisStreamSequenceToken(123, 456);
var token2 = new RedisStreamSequenceToken(123, 457);
// Act
var result = token1.Equals(token2);
// Assert
Assert.False(result);
}
[Fact]
public void CompareTo_ShouldThrowArgumentNullException_ForNullToken()
{
// Arrange
var token = new RedisStreamSequenceToken(123, 456);
// Act & Assert
Assert.Throws<ArgumentNullException>(() => token.CompareTo(null));
}
[Fact]
public void CompareTo_ShouldThrowArgumentException_ForInvalidTokenType()
{
// Arrange
var token = new RedisStreamSequenceToken(123, 456);
var invalidToken = new Mock<StreamSequenceToken>().Object;
// Act & Assert
Assert.Throws<ArgumentException>(() => token.CompareTo(invalidToken));
}
}
}

View File

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Provider\Provider.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>