diff --git a/Directory.Build.props b/Directory.Build.props index 644af0ec2..bf96d72ee 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,6 +7,11 @@ enable + + true + false + + Microsoft.DurableTask diff --git a/samples/AzureFunctionsUnitTests/SampleUnitTests.cs b/samples/AzureFunctionsUnitTests/SampleUnitTests.cs index 85d3a5b7d..9f31d791e 100644 --- a/samples/AzureFunctionsUnitTests/SampleUnitTests.cs +++ b/samples/AzureFunctionsUnitTests/SampleUnitTests.cs @@ -200,9 +200,9 @@ public TestResponse(FunctionContext functionContext) : base(functionContext) public class TestLogger : ILogger { // list of all logs emitted, for validation - public IList CapturedLogs {get; set;} = new List(); + public IList CapturedLogs { get; set; } = new List(); - public IDisposable BeginScope(TState state) => Mock.Of(); + public IDisposable BeginScope(TState state) where TState : notnull => Mock.Of(); public bool IsEnabled(LogLevel logLevel) { diff --git a/samples/LargePayloadConsoleApp/Program.cs b/samples/LargePayloadConsoleApp/Program.cs index 1591d8c04..eb2c22e99 100644 --- a/samples/LargePayloadConsoleApp/Program.cs +++ b/samples/LargePayloadConsoleApp/Program.cs @@ -27,7 +27,7 @@ // Keep threshold small to force externalization for demo purposes opts.ExternalizeThresholdBytes = 1024; // 1KB opts.ConnectionString = builder.Configuration.GetValue("DURABLETASK_STORAGE") ?? "UseDevelopmentStorage=true"; - opts.ContainerName = builder.Configuration.GetValue("DURABLETASK_PAYLOAD_CONTAINER"); + opts.ContainerName = builder.Configuration.GetValue("DURABLETASK_PAYLOAD_CONTAINER") ?? "payloads"; }); // 2) Configure Durable Task client diff --git a/src/Client/Grpc/GrpcDurableTaskClient.cs b/src/Client/Grpc/GrpcDurableTaskClient.cs index efc6d765c..422f15060 100644 --- a/src/Client/Grpc/GrpcDurableTaskClient.cs +++ b/src/Client/Grpc/GrpcDurableTaskClient.cs @@ -110,7 +110,7 @@ public override async Task ScheduleNewOrchestrationInstanceAsync( DateTimeOffset? startAt = options?.StartAt; this.logger.SchedulingOrchestration( - request.InstanceId, + request.InstanceId ?? string.Empty, orchestratorName, sizeInBytes: request.Input != null ? Encoding.UTF8.GetByteCount(request.Input) : 0, startAt.GetValueOrDefault(DateTimeOffset.UtcNow)); diff --git a/src/Extensions/AzureBlobPayloads/Options/LargePayloadStorageOptions.cs b/src/Extensions/AzureBlobPayloads/Options/LargePayloadStorageOptions.cs index 7bd47bfc6..2a8b1995b 100644 --- a/src/Extensions/AzureBlobPayloads/Options/LargePayloadStorageOptions.cs +++ b/src/Extensions/AzureBlobPayloads/Options/LargePayloadStorageOptions.cs @@ -63,7 +63,6 @@ public LargePayloadStorageOptions(Uri accountUri, TokenCredential credential) /// Gets or sets the threshold in bytes at which payloads are externalized. Default is 900_000 bytes. /// Value must not exceed 1 MiB (1,048,576 bytes). /// - public int ExternalizeThresholdBytes { get => this.externalizeThresholdBytes; diff --git a/src/Extensions/AzureBlobPayloads/PayloadStore/BlobPayloadStore.cs b/src/Extensions/AzureBlobPayloads/PayloadStore/BlobPayloadStore.cs index 06294f5f7..9edced26d 100644 --- a/src/Extensions/AzureBlobPayloads/PayloadStore/BlobPayloadStore.cs +++ b/src/Extensions/AzureBlobPayloads/PayloadStore/BlobPayloadStore.cs @@ -89,7 +89,7 @@ public override async Task UploadAsync(string payLoad, CancellationToken // using MemoryStream payloadStream = new(payloadBuffer, writable: false); // await payloadStream.CopyToAsync(compressedBlobStream, bufferSize: DefaultCopyBufferSize, cancellationToken); - await compressedBlobStream.WriteAsync(payloadBuffer, 0, payloadBuffer.Length, cancellationToken); + await WritePayloadAsync(payloadBuffer, compressedBlobStream, cancellationToken); await compressedBlobStream.FlushAsync(cancellationToken); await blobStream.FlushAsync(cancellationToken); } @@ -99,7 +99,7 @@ public override async Task UploadAsync(string payLoad, CancellationToken // using MemoryStream payloadStream = new(payloadBuffer, writable: false); // await payloadStream.CopyToAsync(blobStream, bufferSize: DefaultCopyBufferSize, cancellationToken); - await blobStream.WriteAsync(payloadBuffer, 0, payloadBuffer.Length, cancellationToken); + await WritePayloadAsync(payloadBuffer, blobStream, cancellationToken); await blobStream.FlushAsync(cancellationToken); } @@ -126,11 +126,11 @@ public override async Task DownloadAsync(string token, CancellationToken { using GZipStream decompressed = new(contentStream, CompressionMode.Decompress); using StreamReader reader = new(decompressed, Encoding.UTF8); - return await reader.ReadToEndAsync(); + return await ReadToEndAsync(reader, cancellationToken); } using StreamReader uncompressedReader = new(contentStream, Encoding.UTF8); - return await uncompressedReader.ReadToEndAsync(); + return await ReadToEndAsync(uncompressedReader, cancellationToken); } /// @@ -144,6 +144,27 @@ public override bool IsKnownPayloadToken(string value) return value.StartsWith(TokenPrefix, StringComparison.Ordinal); } + static async Task WritePayloadAsync(byte[] payloadBuffer, Stream target, CancellationToken cancellationToken) + { +#if NETSTANDARD2_0 + await target.WriteAsync(payloadBuffer, 0, payloadBuffer.Length, cancellationToken).ConfigureAwait(false); +#else + await target.WriteAsync(payloadBuffer.AsMemory(0, payloadBuffer.Length), cancellationToken).ConfigureAwait(false); +#endif + } + + static async Task ReadToEndAsync(StreamReader reader, CancellationToken cancellationToken) + { +#if NETSTANDARD2_0 + cancellationToken.ThrowIfCancellationRequested(); + return await reader.ReadToEndAsync().ConfigureAwait(false); +#elif NET8_0_OR_GREATER + return await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); +#else + return await reader.ReadToEndAsync().WaitAsync(cancellationToken).ConfigureAwait(false); +#endif + } + static string EncodeToken(string container, string name) => $"blob:v1:{container}:{name}"; static (string Container, string Name) DecodeToken(string token) diff --git a/src/InProcessTestHost/DurableTaskTestHost.cs b/src/InProcessTestHost/DurableTaskTestHost.cs index ec075b945..931912eff 100644 --- a/src/InProcessTestHost/DurableTaskTestHost.cs +++ b/src/InProcessTestHost/DurableTaskTestHost.cs @@ -22,7 +22,7 @@ namespace Microsoft.DurableTask.Testing; /// public sealed class DurableTaskTestHost : IAsyncDisposable { - readonly IWebHost sidecarHost; + readonly IHost sidecarHost; readonly IHost workerHost; readonly GrpcChannel grpcChannel; @@ -33,7 +33,7 @@ public sealed class DurableTaskTestHost : IAsyncDisposable /// The worker host. /// The gRPC channel. /// The durable task client. - public DurableTaskTestHost(IWebHost sidecarHost, IHost workerHost, GrpcChannel grpcChannel, DurableTaskClient client) + public DurableTaskTestHost(IHost sidecarHost, IHost workerHost, GrpcChannel grpcChannel, DurableTaskClient client) { this.sidecarHost = sidecarHost; this.workerHost = workerHost; @@ -68,32 +68,37 @@ public static async Task StartAsync( ? $"http://localhost:{options.Port.Value}" : $"http://localhost:{Random.Shared.Next(30000, 40000)}"; - var sidecarHost = new WebHostBuilder() - .UseKestrel(kestrelOptions => + IHost sidecarHost = Host.CreateDefaultBuilder() + .ConfigureWebHostDefaults(webBuilder => { - // Configure for HTTP/2 (required for gRPC) - kestrelOptions.ConfigureEndpointDefaults(listenOptions => - listenOptions.Protocols = HttpProtocols.Http2); - }) - .UseUrls(address) - .ConfigureServices(services => - { - services.AddGrpc(); - services.AddSingleton(orchestrationService); - services.AddSingleton(orchestrationService); - services.AddSingleton(); - }) - .Configure(app => - { - app.UseRouting(); - app.UseEndpoints(endpoints => + webBuilder.UseUrls(address); + webBuilder.ConfigureKestrel(kestrelOptions => + { + // Configure for HTTP/2 (required for gRPC) + kestrelOptions.ConfigureEndpointDefaults(listenOptions => + listenOptions.Protocols = HttpProtocols.Http2); + }); + + webBuilder.ConfigureServices(services => + { + services.AddGrpc(); + services.AddSingleton(orchestrationService); + services.AddSingleton(orchestrationService); + services.AddSingleton(); + }); + + webBuilder.Configure(app => { - endpoints.MapGrpcService(); + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapGrpcService(); + }); }); }) .Build(); - sidecarHost.Start(); + await sidecarHost.StartAsync(cancellationToken); var grpcChannel = GrpcChannel.ForAddress(address); // Create worker host with user's orchestrators and activities diff --git a/src/InProcessTestHost/Sidecar/Grpc/TaskHubGrpcServer.cs b/src/InProcessTestHost/Sidecar/Grpc/TaskHubGrpcServer.cs index b25177ae9..15f65dd84 100644 --- a/src/InProcessTestHost/Sidecar/Grpc/TaskHubGrpcServer.cs +++ b/src/InProcessTestHost/Sidecar/Grpc/TaskHubGrpcServer.cs @@ -471,20 +471,20 @@ static P.GetInstanceResponse CreateGetInstanceResponse(OrchestrationState state, { // Get the original orchestration state IList states = await this.client.GetOrchestrationStateAsync(request.InstanceId, false); - + if (states == null || states.Count == 0) { throw new RpcException(new Status(StatusCode.NotFound, $"An orchestration with the instanceId {request.InstanceId} was not found.")); } OrchestrationState state = states[0]; - + // Check if the state is null if (state == null) { throw new RpcException(new Status(StatusCode.NotFound, $"An orchestration with the instanceId {request.InstanceId} was not found.")); } - + string newInstanceId = request.RestartWithNewInstanceId ? Guid.NewGuid().ToString("N") : request.InstanceId; // Create a new orchestration instance @@ -646,6 +646,13 @@ public override async Task GetWorkItems(P.GetWorkItemsRequest request, IServerSt } } + /// + /// Streams the instance history for a given orchestration instance to the client in chunked form. + /// + /// The history request that identifies the instance. + /// The response stream used to write history chunks. + /// The server call context for the streaming operation. + /// A task that completes when streaming finishes. public override async Task StreamInstanceHistory(P.StreamInstanceHistoryRequest request, IServerStreamWriter responseStream, ServerCallContext context) { if (this.streamingPastEvents.TryGetValue(request.InstanceId, out List? pastEvents)) diff --git a/src/Shared/AzureManaged/DurableTaskVersionUtil.cs b/src/Shared/AzureManaged/DurableTaskVersionUtil.cs index 7ad3bc47c..873931292 100644 --- a/src/Shared/AzureManaged/DurableTaskVersionUtil.cs +++ b/src/Shared/AzureManaged/DurableTaskVersionUtil.cs @@ -3,6 +3,8 @@ using System.Diagnostics; +#pragma warning disable CS0436 + namespace Microsoft.DurableTask; /// @@ -18,7 +20,7 @@ public static class DurableTaskUserAgentUtil /// /// The version of the SDK used in the user agent string. /// - static readonly string PackageVersion = FileVersionInfo.GetVersionInfo(typeof(DurableTaskUserAgentUtil).Assembly.Location).FileVersion; + static readonly string PackageVersion = FileVersionInfo.GetVersionInfo(typeof(DurableTaskUserAgentUtil).Assembly.Location).FileVersion ?? "unknown"; /// /// Generates the user agent string for the Durable Task SDK based on a fixed name, the package version, and the caller type. @@ -29,4 +31,5 @@ public static string GetUserAgent(string callerType) { return $"{SdkName}/{PackageVersion?.ToString() ?? "unknown"} ({callerType})"; } -} \ No newline at end of file +} +#pragma warning restore CS0436 \ No newline at end of file diff --git a/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs b/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs index 7aad801c3..3c760ddf6 100644 --- a/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs +++ b/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs @@ -95,7 +95,7 @@ public void UseDurableTaskScheduler_WithLocalhostConnectionString_ShouldConfigur [Theory] [InlineData(null, "testhub")] [InlineData("myaccount.westus3.durabletask.io", null)] - public void UseDurableTaskScheduler_WithNullParameters_ShouldThrowOptionsValidationException(string endpoint, string taskHub) + public void UseDurableTaskScheduler_WithNullParameters_ShouldThrowOptionsValidationException(string? endpoint, string? taskHub) { // Arrange ServiceCollection services = new ServiceCollection(); @@ -104,7 +104,7 @@ public void UseDurableTaskScheduler_WithNullParameters_ShouldThrowOptionsValidat DefaultAzureCredential credential = new DefaultAzureCredential(); // Act - mockBuilder.Object.UseDurableTaskScheduler(endpoint, taskHub, credential); + mockBuilder.Object.UseDurableTaskScheduler(endpoint!, taskHub!, credential); ServiceProvider provider = services.BuildServiceProvider(); // Assert @@ -157,7 +157,7 @@ public void UseDurableTaskScheduler_WithInvalidConnectionString_ShouldThrowArgum [Theory] [InlineData("")] [InlineData(null)] - public void UseDurableTaskScheduler_WithNullOrEmptyConnectionString_ShouldThrowArgumentException(string connectionString) + public void UseDurableTaskScheduler_WithNullOrEmptyConnectionString_ShouldThrowArgumentException(string? connectionString) { // Arrange ServiceCollection services = new ServiceCollection(); @@ -165,7 +165,7 @@ public void UseDurableTaskScheduler_WithNullOrEmptyConnectionString_ShouldThrowA mockBuilder.Setup(b => b.Services).Returns(services); // Act & Assert - Action action = () => mockBuilder.Object.UseDurableTaskScheduler(connectionString); + Action action = () => mockBuilder.Object.UseDurableTaskScheduler(connectionString!); action.Should().Throw(); } diff --git a/test/Client/OrchestrationServiceClientShim.Tests/ShimDurableTaskClientTests.cs b/test/Client/OrchestrationServiceClientShim.Tests/ShimDurableTaskClientTests.cs index 0d14b3961..fa65cc42a 100644 --- a/test/Client/OrchestrationServiceClientShim.Tests/ShimDurableTaskClientTests.cs +++ b/test/Client/OrchestrationServiceClientShim.Tests/ShimDurableTaskClientTests.cs @@ -94,7 +94,7 @@ public void Ctor_EntitiesConfigured_GetClientSuccess() [Theory] [InlineData(false)] [InlineData(true)] - public async void GetInstanceMetadata_EmptyList_Null(bool isNull) + public async Task GetInstanceMetadata_EmptyList_Null(bool isNull) { // arrange List? states = isNull ? null : new(); diff --git a/test/Grpc.IntegrationTests/ExternalEventStackTests.cs b/test/Grpc.IntegrationTests/ExternalEventStackTests.cs index dcd79b65d..bd028a2fd 100644 --- a/test/Grpc.IntegrationTests/ExternalEventStackTests.cs +++ b/test/Grpc.IntegrationTests/ExternalEventStackTests.cs @@ -36,39 +36,39 @@ public async Task StackBehavior_LIFO_NewestWaiterReceivesEventFirst() { // First waiter Task firstWaiter = ctx.WaitForExternalEvent(EventName); - + // Second waiter (newer, should receive event first) Task secondWaiter = ctx.WaitForExternalEvent(EventName); - + // Wait for both events to arrive from external client string secondResult = await secondWaiter; // Should receive first event (stack top) string firstResult = await firstWaiter; // Should receive second event - + // Return which waiter received which event return $"{firstResult}:{secondResult}"; })); }); string instanceId = await server.Client.ScheduleNewOrchestrationInstanceAsync(orchestratorName); - + // Wait for orchestration to start and set up waiters OrchestrationMetadata metadata = await server.Client.WaitForInstanceStartAsync( - instanceId, this.TimeoutToken); - + instanceId, this.TimeoutToken) + ?? throw new InvalidOperationException("Orchestration did not start."); + // Send first event - should be received by second waiter (stack top) await server.Client.RaiseEventAsync(instanceId, EventName, SecondEventPayload); - + // Send second event - should be received by first waiter await server.Client.RaiseEventAsync(instanceId, EventName, FirstEventPayload); metadata = await server.Client.WaitForInstanceCompletionAsync( - instanceId, getInputsAndOutputs: true, this.TimeoutToken); - - Assert.NotNull(metadata); + instanceId, getInputsAndOutputs: true, this.TimeoutToken) + ?? throw new InvalidOperationException("Orchestration did not complete."); Assert.Equal(OrchestrationRuntimeStatus.Completed, metadata.RuntimeStatus); - + // Verify the output: second waiter should have received "second-event", first waiter "first-event" - string result = metadata.ReadOutputAs(); + string result = metadata.ReadOutputAs() ?? throw new InvalidOperationException("Missing orchestration output."); Assert.Equal($"{FirstEventPayload}:{SecondEventPayload}", result); } @@ -110,7 +110,7 @@ public async Task Issue508_FirstWaiterCancelled_SecondWaiterReceivesEvent() { Task waitForEvent = ctx.WaitForExternalEvent(EventName, cts.Token); Task timeout = ctx.CreateTimer(ctx.CurrentUtcDateTime.AddSeconds(2), CancellationToken.None); - + Task winner = await Task.WhenAny(waitForEvent, timeout); if (winner == timeout) { @@ -125,21 +125,20 @@ public async Task Issue508_FirstWaiterCancelled_SecondWaiterReceivesEvent() }); string instanceId = await server.Client.ScheduleNewOrchestrationInstanceAsync(orchestratorName); - + // Wait for orchestration to start and cancel first waiter await server.Client.WaitForInstanceStartAsync( instanceId, this.TimeoutToken); - + // Send event - should be received by second waiter (stack top), not the cancelled first waiter await server.Client.RaiseEventAsync(instanceId, EventName, EventPayload); OrchestrationMetadata metadata = await server.Client.WaitForInstanceCompletionAsync( - instanceId, getInputsAndOutputs: true, this.TimeoutToken); - - Assert.NotNull(metadata); + instanceId, getInputsAndOutputs: true, this.TimeoutToken) + ?? throw new InvalidOperationException("Orchestration did not complete."); Assert.Equal(OrchestrationRuntimeStatus.Completed, metadata.RuntimeStatus); - - string result = metadata.ReadOutputAs(); + + string result = metadata.ReadOutputAs() ?? throw new InvalidOperationException("Missing orchestration output."); Assert.Equal(EventPayload, result); } @@ -173,24 +172,23 @@ public async Task MultipleEvents_MultipleWaiters_LIFOOrder() }); string instanceId = await server.Client.ScheduleNewOrchestrationInstanceAsync(orchestratorName); - + // Wait for orchestration to start and set up waiters await server.Client.WaitForInstanceStartAsync( instanceId, this.TimeoutToken); - + // Send three events - should be received in LIFO order await server.Client.RaiseEventAsync(instanceId, EventName, "event-1"); await server.Client.RaiseEventAsync(instanceId, EventName, "event-2"); await server.Client.RaiseEventAsync(instanceId, EventName, "event-3"); OrchestrationMetadata metadata = await server.Client.WaitForInstanceCompletionAsync( - instanceId, getInputsAndOutputs: true, this.TimeoutToken); - - Assert.NotNull(metadata); + instanceId, getInputsAndOutputs: true, this.TimeoutToken) + ?? throw new InvalidOperationException("Orchestration did not complete."); Assert.Equal(OrchestrationRuntimeStatus.Completed, metadata.RuntimeStatus); - + // Verify LIFO order: waiter3 (newest) gets event-1, waiter2 gets event-2, waiter1 gets event-3 - string result = metadata.ReadOutputAs(); + string result = metadata.ReadOutputAs() ?? throw new InvalidOperationException("Missing orchestration output."); Assert.Equal("event-3,event-2,event-1", result); } @@ -215,7 +213,7 @@ public async Task EventBuffering_EventArrivesBeforeWaiter_FirstWaiterReceivesBuf // Now create waiter - should immediately receive the buffered event Task waiter = ctx.WaitForExternalEvent(EventName); Task timeout = ctx.CreateTimer(ctx.CurrentUtcDateTime.AddSeconds(1), CancellationToken.None); - + Task winner = await Task.WhenAny(waiter, timeout); if (winner == timeout) { @@ -228,17 +226,16 @@ public async Task EventBuffering_EventArrivesBeforeWaiter_FirstWaiterReceivesBuf }); string instanceId = await server.Client.ScheduleNewOrchestrationInstanceAsync(orchestratorName); - + // Send event before waiter is created await server.Client.RaiseEventAsync(instanceId, EventName, EventPayload); OrchestrationMetadata metadata = await server.Client.WaitForInstanceCompletionAsync( - instanceId, getInputsAndOutputs: true, this.TimeoutToken); - - Assert.NotNull(metadata); + instanceId, getInputsAndOutputs: true, this.TimeoutToken) + ?? throw new InvalidOperationException("Orchestration did not complete."); Assert.Equal(OrchestrationRuntimeStatus.Completed, metadata.RuntimeStatus); - - string result = metadata.ReadOutputAs(); + + string result = metadata.ReadOutputAs() ?? throw new InvalidOperationException("Missing orchestration output."); Assert.Equal(EventPayload, result); } } diff --git a/test/Grpc.IntegrationTests/GrpcSidecarFixture.cs b/test/Grpc.IntegrationTests/GrpcSidecarFixture.cs index 2408d3d27..cfefcd4d3 100644 --- a/test/Grpc.IntegrationTests/GrpcSidecarFixture.cs +++ b/test/Grpc.IntegrationTests/GrpcSidecarFixture.cs @@ -11,6 +11,7 @@ using Microsoft.DurableTask.Worker; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Hosting; namespace Microsoft.DurableTask.Grpc.Tests; @@ -18,7 +19,7 @@ public sealed class GrpcSidecarFixture : IDisposable { const string ListenHost = "localhost"; - readonly IWebHost host; + readonly IHost host; public GrpcSidecarFixture() { @@ -26,28 +27,32 @@ public GrpcSidecarFixture() // Use a random port number to allow multiple instances to run in parallel string address = $"http://{ListenHost}:{Random.Shared.Next(30000, 40000)}"; - this.host = new WebHostBuilder() - .UseKestrel(options => + this.host = Host.CreateDefaultBuilder() + .ConfigureWebHostDefaults(webBuilder => { - // Need to force Http2 in Kestrel in unencrypted scenarios - // https://docs.microsoft.com/en-us/aspnet/core/grpc/troubleshoot?view=aspnetcore-3.0 - options.ConfigureEndpointDefaults(listenOptions => listenOptions.Protocols = HttpProtocols.Http2); - }) - .UseUrls(address) - .ConfigureServices(services => - { - services.AddGrpc(); - services.AddSingleton(service); - services.AddSingleton(service); - services.AddSingleton(); - }) - .Configure(app => - { - app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapGrpcService(); - }); + webBuilder + .UseKestrel(options => + { + // Need to force Http2 in Kestrel in unencrypted scenarios + // https://docs.microsoft.com/en-us/aspnet/core/grpc/troubleshoot?view=aspnetcore-3.0 + options.ConfigureEndpointDefaults(listenOptions => listenOptions.Protocols = HttpProtocols.Http2); + }) + .UseUrls(address) + .ConfigureServices(services => + { + services.AddGrpc(); + services.AddSingleton(service); + services.AddSingleton(service); + services.AddSingleton(); + }) + .Configure(app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapGrpcService(); + }); + }); }) .Build(); diff --git a/test/Grpc.IntegrationTests/OrchestrationErrorHandling.cs b/test/Grpc.IntegrationTests/OrchestrationErrorHandling.cs index 84c2f1e49..423dbb2f4 100644 --- a/test/Grpc.IntegrationTests/OrchestrationErrorHandling.cs +++ b/test/Grpc.IntegrationTests/OrchestrationErrorHandling.cs @@ -262,8 +262,8 @@ public async Task RetryActivityFailuresCustomLogic(int expectedNumberOfAttempts, [Theory] [InlineData(10, typeof(ApplicationException), false, int.MaxValue, 2, 1, OrchestrationRuntimeStatus.Failed)] // 1 attempt since retry timeout expired. [InlineData(2, typeof(ApplicationException), false, int.MaxValue, null, 1, OrchestrationRuntimeStatus.Failed)] // 1 attempt since handler specifies no retry. - [InlineData(2, typeof(CustomException),true, int.MaxValue, null, 2, OrchestrationRuntimeStatus.Failed)] // 2 attempts, custom exception type - [InlineData(10, typeof(XunitException),true, 4, null, 5, OrchestrationRuntimeStatus.Completed)] // 10 attempts, 3rd party exception type + [InlineData(2, typeof(CustomException), true, int.MaxValue, null, 2, OrchestrationRuntimeStatus.Failed)] // 2 attempts, custom exception type + [InlineData(10, typeof(XunitException), true, 4, null, 5, OrchestrationRuntimeStatus.Completed)] // 10 attempts, 3rd party exception type public async Task RetryActivityFailuresCustomLogicAndPolicy( int maxNumberOfAttempts, Type exceptionType, @@ -665,7 +665,7 @@ void MyActivityImpl(TaskActivityContext ctx) => { // Register the custom exception properties provider b.Services.AddSingleton(); - + b.AddTasks(tasks => tasks .AddOrchestratorFunc(orchestratorName, MyOrchestrationImpl) .AddActivityFunc(activityName, MyActivityImpl)); @@ -753,7 +753,7 @@ void MyOrchestrationImpl(TaskOrchestrationContext ctx) => { // Register the custom exception properties provider b.Services.AddSingleton(); - + b.AddTasks(tasks => tasks .AddOrchestratorFunc(orchestratorName, MyOrchestrationImpl)); }); @@ -814,7 +814,7 @@ void ActivityImpl(TaskActivityContext ctx) => { // Register the custom exception properties provider b.Services.AddSingleton(); - + b.AddTasks(tasks => tasks .AddOrchestratorFunc(parentOrchestratorName, ParentOrchestrationImpl) .AddOrchestratorFunc(subOrchestratorName, SubOrchestrationImpl) @@ -831,7 +831,7 @@ void ActivityImpl(TaskActivityContext ctx) => Assert.NotNull(metadata.FailureDetails); TaskFailureDetails failureDetails = metadata.FailureDetails!; - + // The parent orchestration failed due to a TaskFailedException from the sub-orchestration Assert.Equal(typeof(TaskFailedException).FullName, failureDetails.ErrorType); Assert.Contains(subOrchestratorName, failureDetails.ErrorMessage); @@ -879,10 +879,12 @@ public CustomException(string message, Exception innerException) { } +#pragma warning disable SYSLIB0051 protected CustomException(SerializationInfo info, StreamingContext context) : base(info, context) { } +#pragma warning restore SYSLIB0051 } /// @@ -891,7 +893,7 @@ protected CustomException(SerializationInfo info, StreamingContext context) [Serializable] class BusinessValidationException : Exception { - public BusinessValidationException(string message, + public BusinessValidationException(string message, string stringProperty, int intProperty, long longProperty, @@ -917,10 +919,12 @@ public BusinessValidationException(string message, public IList? ListProperty { get; } public object? NullProperty { get; } +#pragma warning disable SYSLIB0051 protected BusinessValidationException(SerializationInfo info, StreamingContext context) : base(info, context) { } +#pragma warning restore SYSLIB0051 } // Set a custom exception provider. diff --git a/test/Grpc.IntegrationTests/OrchestrationPatterns.cs b/test/Grpc.IntegrationTests/OrchestrationPatterns.cs index 1315eeff9..666d7e253 100644 --- a/test/Grpc.IntegrationTests/OrchestrationPatterns.cs +++ b/test/Grpc.IntegrationTests/OrchestrationPatterns.cs @@ -1142,9 +1142,11 @@ class OrchestrationFilter : IOrchestrationFilter public ValueTask IsOrchestrationValidAsync(OrchestrationFilterParameters info, CancellationToken cancellationToken = default) { - return ValueTask.FromResult( - !this.NameDenySet.Contains(info.Name) - && !this.TagDenyDict.Any(kvp => info.Tags != null && info.Tags.ContainsKey(kvp.Key) && info.Tags[kvp.Key] == kvp.Value)); + bool nameAllowed = info.Name is string name && !this.NameDenySet.Contains(name); + bool tagsAllowed = info.Tags == null + || !this.TagDenyDict.Any(kvp => info.Tags.TryGetValue(kvp.Key, out string? value) && value == kvp.Value); + + return ValueTask.FromResult(nameAllowed && tagsAllowed); } } diff --git a/test/ScheduledTasks.Tests/Client/DefaultScheduleClientTests.cs b/test/ScheduledTasks.Tests/Client/DefaultScheduleClientTests.cs index d5c89fa0e..88253c780 100644 --- a/test/ScheduledTasks.Tests/Client/DefaultScheduleClientTests.cs +++ b/test/ScheduledTasks.Tests/Client/DefaultScheduleClientTests.cs @@ -39,11 +39,11 @@ public void Constructor_WithNullClient_ThrowsArgumentNullException() [Theory] [InlineData(null, typeof(ArgumentNullException), "Value cannot be null")] [InlineData("", typeof(ArgumentException), "Parameter cannot be an empty string")] - public void Constructor_WithInvalidScheduleId_ThrowsCorrectException(string invalidScheduleId, Type expectedExceptionType, string expectedMessage) + public void Constructor_WithInvalidScheduleId_ThrowsCorrectException(string? invalidScheduleId, Type expectedExceptionType, string expectedMessage) { // Act & Assert var ex = Assert.Throws(expectedExceptionType, () => - new DefaultScheduleClient(this.durableTaskClient.Object, invalidScheduleId, this.logger)); + new DefaultScheduleClient(this.durableTaskClient.Object, invalidScheduleId!, this.logger)); Assert.Contains(expectedMessage, ex.Message, StringComparison.OrdinalIgnoreCase); } @@ -192,7 +192,8 @@ public async Task PauseAsync_ExecutesPauseOperation() this.durableTaskClient .Setup(c => c.WaitForInstanceCompletionAsync(instanceId, true, It.IsAny())) - .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId) { + .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId) + { RuntimeStatus = OrchestrationRuntimeStatus.Completed }); @@ -228,7 +229,8 @@ public async Task ResumeAsync_ExecutesResumeOperation() this.durableTaskClient .Setup(c => c.WaitForInstanceCompletionAsync(instanceId, true, It.IsAny())) - .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId) { + .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId) + { RuntimeStatus = OrchestrationRuntimeStatus.Completed }); @@ -269,7 +271,8 @@ public async Task UpdateAsync_ExecutesUpdateOperation() this.durableTaskClient .Setup(c => c.WaitForInstanceCompletionAsync(instanceId, true, It.IsAny())) - .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId) { + .ReturnsAsync(new OrchestrationMetadata(nameof(ExecuteScheduleOperationOrchestrator), instanceId) + { RuntimeStatus = OrchestrationRuntimeStatus.Completed }); diff --git a/test/ScheduledTasks.Tests/Client/DefaultScheduledTaskClientTests.cs b/test/ScheduledTasks.Tests/Client/DefaultScheduledTaskClientTests.cs index 4eeada246..b38b613cd 100644 --- a/test/ScheduledTasks.Tests/Client/DefaultScheduledTaskClientTests.cs +++ b/test/ScheduledTasks.Tests/Client/DefaultScheduledTaskClientTests.cs @@ -59,10 +59,10 @@ public void GetScheduleClient_ReturnsValidClient() [Theory] [InlineData(null, typeof(ArgumentNullException), "Value cannot be null")] [InlineData("", typeof(ArgumentException), "Parameter cannot be an empty string")] - public void GetScheduleClient_WithInvalidId_ThrowsCorrectException(string scheduleId, Type expectedExceptionType, string expectedMessage) + public void GetScheduleClient_WithInvalidId_ThrowsCorrectException(string? scheduleId, Type expectedExceptionType, string expectedMessage) { // Act & Assert - var ex = Assert.Throws(expectedExceptionType, () => this.client.GetScheduleClient(scheduleId)); + var ex = Assert.Throws(expectedExceptionType, () => this.client.GetScheduleClient(scheduleId!)); Assert.Contains(expectedMessage, ex.Message, StringComparison.OrdinalIgnoreCase); } @@ -100,13 +100,7 @@ public async Task CreateScheduleAsync_WithValidOptions_CreatesSchedule() this.durableTaskClient.Verify( x => x.ScheduleNewOrchestrationInstanceAsync( It.Is(n => n.Name == nameof(ExecuteScheduleOperationOrchestrator)), - It.Is(r => - r.EntityId.Name == entityInstanceId.Name && - r.EntityId.Key == entityInstanceId.Key && - r.OperationName == nameof(Schedule.CreateSchedule) && - ((ScheduleCreationOptions)r.Input).ScheduleId == options.ScheduleId && - ((ScheduleCreationOptions)r.Input).OrchestrationName == options.OrchestrationName && - ((ScheduleCreationOptions)r.Input).Interval == options.Interval), + It.Is(r => MatchesCreateScheduleRequest(r, entityInstanceId, options)), default), Times.Once); } @@ -294,4 +288,26 @@ public async Task ListSchedulesAsync_ReturnsSchedules() Assert.All(schedules, s => Assert.NotNull(s.LastRunAt)); Assert.All(schedules, s => Assert.NotNull(s.NextRunAt)); } + + static bool MatchesCreateScheduleRequest(ScheduleOperationRequest request, EntityInstanceId expectedEntityId, ScheduleCreationOptions expectedOptions) + { + if (request.EntityId.Name != expectedEntityId.Name || request.EntityId.Key != expectedEntityId.Key) + { + return false; + } + + if (!string.Equals(request.OperationName, nameof(Schedule.CreateSchedule), StringComparison.Ordinal)) + { + return false; + } + + if (request.Input is not ScheduleCreationOptions createOptions) + { + return false; + } + + return createOptions.ScheduleId == expectedOptions.ScheduleId + && createOptions.OrchestrationName == expectedOptions.OrchestrationName + && createOptions.Interval == expectedOptions.Interval; + } } \ No newline at end of file diff --git a/test/ScheduledTasks.Tests/Models/ScheduleConfigurationTests.cs b/test/ScheduledTasks.Tests/Models/ScheduleConfigurationTests.cs index b5bf28817..998dfd3fd 100644 --- a/test/ScheduledTasks.Tests/Models/ScheduleConfigurationTests.cs +++ b/test/ScheduledTasks.Tests/Models/ScheduleConfigurationTests.cs @@ -29,13 +29,13 @@ public void Constructor_WithValidParameters_CreatesInstance() [InlineData("", "orchestration", typeof(ArgumentException), "Parameter cannot be an empty string or start with the null character")] [InlineData("schedule", null, typeof(ArgumentNullException), "Value cannot be null")] [InlineData("schedule", "", typeof(ArgumentException), "Parameter cannot be an empty string or start with the null character")] - public void Constructor_WithInvalidParameters_ThrowsException(string scheduleId, string orchestrationName, Type expectedExceptionType, string expectedMessage) + public void Constructor_WithInvalidParameters_ThrowsException(string? scheduleId, string? orchestrationName, Type expectedExceptionType, string expectedMessage) { // Arrange TimeSpan interval = TimeSpan.FromMinutes(5); // Act & Assert - var ex = Assert.Throws(expectedExceptionType, () => new ScheduleConfiguration(scheduleId, orchestrationName, interval)); + var ex = Assert.Throws(expectedExceptionType, () => new ScheduleConfiguration(scheduleId!, orchestrationName!, interval)); Assert.Contains(expectedMessage, ex.Message); } diff --git a/test/ScheduledTasks.Tests/Models/ScheduleCreationOptionsTests.cs b/test/ScheduledTasks.Tests/Models/ScheduleCreationOptionsTests.cs index 10d21a6b4..3f6a12aba 100644 --- a/test/ScheduledTasks.Tests/Models/ScheduleCreationOptionsTests.cs +++ b/test/ScheduledTasks.Tests/Models/ScheduleCreationOptionsTests.cs @@ -35,8 +35,8 @@ public void Constructor_WithValidParameters_CreatesInstance() [InlineData("schedule", null, typeof(ArgumentNullException), "Value cannot be null.")] [InlineData("schedule", "", typeof(ArgumentException), "Parameter cannot be an empty string or start with the null character")] public void Constructor_WithInvalidParameters_ThrowsArgumentException( - string scheduleId, - string orchestrationName, + string? scheduleId, + string? orchestrationName, Type expectedExceptionType, string expectedMessage) { @@ -44,7 +44,7 @@ public void Constructor_WithInvalidParameters_ThrowsArgumentException( TimeSpan interval = TimeSpan.FromMinutes(5); // Act & Assert - var exception = Assert.Throws(expectedExceptionType, () => new ScheduleCreationOptions(scheduleId, orchestrationName, interval)); + var exception = Assert.Throws(expectedExceptionType, () => new ScheduleCreationOptions(scheduleId!, orchestrationName!, interval)); Assert.Contains(expectedMessage, exception.Message); } diff --git a/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs b/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs index 49e8df4e3..a661b53d3 100644 --- a/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs +++ b/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs @@ -98,7 +98,7 @@ public void UseDurableTaskScheduler_WithLocalhostConnectionString_ShouldConfigur [Theory] [InlineData(null, "testhub")] [InlineData("myaccount.westus3.durabletask.io", null)] - public void UseDurableTaskScheduler_WithNullParameters_ShouldThrowOptionsValidationException(string endpoint, string taskHub) + public void UseDurableTaskScheduler_WithNullParameters_ShouldThrowOptionsValidationException(string? endpoint, string? taskHub) { // Arrange ServiceCollection services = new ServiceCollection(); @@ -107,13 +107,13 @@ public void UseDurableTaskScheduler_WithNullParameters_ShouldThrowOptionsValidat DefaultAzureCredential credential = new DefaultAzureCredential(); // Act - mockBuilder.Object.UseDurableTaskScheduler(endpoint, taskHub, credential); + mockBuilder.Object.UseDurableTaskScheduler(endpoint!, taskHub!, credential); ServiceProvider provider = services.BuildServiceProvider(); // Assert var action = () => provider.GetRequiredService>().Value; action.Should().Throw() - .WithMessage(endpoint == null + .WithMessage(endpoint == null ? "DataAnnotation validation failed for 'DurableTaskSchedulerWorkerOptions' members: 'EndpointAddress' with the error: 'Endpoint address is required'." : "DataAnnotation validation failed for 'DurableTaskSchedulerWorkerOptions' members: 'TaskHubName' with the error: 'Task hub name is required'."); } @@ -161,7 +161,7 @@ public void UseDurableTaskScheduler_WithInvalidConnectionString_ShouldThrowArgum [Theory] [InlineData("")] [InlineData(null)] - public void UseDurableTaskScheduler_WithNullOrEmptyConnectionString_ShouldThrowArgumentException(string connectionString) + public void UseDurableTaskScheduler_WithNullOrEmptyConnectionString_ShouldThrowArgumentException(string? connectionString) { // Arrange ServiceCollection services = new ServiceCollection(); @@ -169,7 +169,7 @@ public void UseDurableTaskScheduler_WithNullOrEmptyConnectionString_ShouldThrowA mockBuilder.Setup(b => b.Services).Returns(services); // Act & Assert - Action action = () => mockBuilder.Object.UseDurableTaskScheduler(connectionString); + Action action = () => mockBuilder.Object.UseDurableTaskScheduler(connectionString!); action.Should().Throw(); } diff --git a/test/Worker/Grpc.Tests/GrpcOrchestrationRunnerTests.cs b/test/Worker/Grpc.Tests/GrpcOrchestrationRunnerTests.cs index 6c3179211..f680ef256 100644 --- a/test/Worker/Grpc.Tests/GrpcOrchestrationRunnerTests.cs +++ b/test/Worker/Grpc.Tests/GrpcOrchestrationRunnerTests.cs @@ -39,7 +39,7 @@ public void EmptyHistory_Returns_NeedsHistoryInResponse() orchestratorRequest.Properties.Add(new MapField() { { "IncludePastEvents", Value.ForBool(false) }}); byte[] requestBytes = orchestratorRequest.ToByteArray(); - string requestString = Convert.ToBase64String(requestBytes); + string requestString = Convert.ToBase64String(requestBytes); string stringResponse = GrpcOrchestrationRunner.LoadAndRun(requestString, new SimpleOrchestrator(), extendedSessions); Protobuf.OrchestratorResponse response = Protobuf.OrchestratorResponse.Parser.ParseFrom(Convert.FromBase64String(stringResponse)); Assert.True(response.RequiresHistory); @@ -327,7 +327,7 @@ public void ExternallyEndedExtendedSession_Evicted() } [Fact] - public async void Stale_ExtendedSessions_Evicted_Async() + public async Task Stale_ExtendedSessions_Evicted_Async() { using var extendedSessions = new ExtendedSessionsCache(); int extendedSessionIdleTimeout = 5; diff --git a/test/Worker/Grpc.Tests/RunBackgroundTaskLoggingTests.cs b/test/Worker/Grpc.Tests/RunBackgroundTaskLoggingTests.cs index 56ace8e77..2a907865b 100644 --- a/test/Worker/Grpc.Tests/RunBackgroundTaskLoggingTests.cs +++ b/test/Worker/Grpc.Tests/RunBackgroundTaskLoggingTests.cs @@ -343,7 +343,7 @@ static async Task AssertEventually(Func condition, int timeoutMs = 2000) } await Task.Delay(50); } - Assert.True(false, "Condition not met within timeout"); + Assert.Fail("Condition not met within timeout"); } sealed class TestFixture : IAsyncDisposable