From 60ca43a24b3d7472e653f1f8af7519c3ed56e525 Mon Sep 17 00:00:00 2001 From: Peter Rang Date: Tue, 21 May 2019 17:31:41 +0200 Subject: [PATCH 1/3] added functionality to unregister message handlers --- .../src/Core/IReceiverClient.cs | 8 ++ .../src/Core/MessageReceiver.cs | 49 ++++++-- .../src/MessageReceivePump.cs | 11 +- .../src/QueueClient.cs | 10 ++ .../src/SubscriptionClient.cs | 10 ++ ...rovals.ApproveAzureServiceBus.approved.txt | 4 + .../tests/MessageReceiverTests.cs | 119 ++++++++++++++++++ .../tests/SenderReceiverClientTestBase.cs | 97 ++++++++++++++ 8 files changed, 298 insertions(+), 10 deletions(-) create mode 100644 sdk/servicebus/Microsoft.Azure.ServiceBus/tests/MessageReceiverTests.cs diff --git a/sdk/servicebus/Microsoft.Azure.ServiceBus/src/Core/IReceiverClient.cs b/sdk/servicebus/Microsoft.Azure.ServiceBus/src/Core/IReceiverClient.cs index 7116ce895fbaf..f03c43e167342 100644 --- a/sdk/servicebus/Microsoft.Azure.ServiceBus/src/Core/IReceiverClient.cs +++ b/sdk/servicebus/Microsoft.Azure.ServiceBus/src/Core/IReceiverClient.cs @@ -63,6 +63,14 @@ public interface IReceiverClient : IClientEntity /// Enable prefetch to speed up the receive rate. void RegisterMessageHandler(Func handler, MessageHandlerOptions messageHandlerOptions); + /// + /// Cancels the continuuous reception of messages without closing the underlying service bus connection and unregisters the message handler. + /// Register a message handler first, using + /// or . + /// Although the message handler is unregistered, active threads are not cancelled. + /// + void UnregisterMessageHandler(); + /// /// Completes a using its lock token. This will delete the message from the queue. /// diff --git a/sdk/servicebus/Microsoft.Azure.ServiceBus/src/Core/MessageReceiver.cs b/sdk/servicebus/Microsoft.Azure.ServiceBus/src/Core/MessageReceiver.cs index a50996a209177..ac048b561b7f8 100644 --- a/sdk/servicebus/Microsoft.Azure.ServiceBus/src/Core/MessageReceiver.cs +++ b/sdk/servicebus/Microsoft.Azure.ServiceBus/src/Core/MessageReceiver.cs @@ -50,11 +50,15 @@ public class MessageReceiver : ClientEntity, IMessageReceiver readonly object messageReceivePumpSyncLock; readonly ActiveClientLinkManager clientLinkManager; readonly ServiceBusDiagnosticSource diagnosticSource; - int prefetchCount; long lastPeekedSequenceNumber; MessageReceivePump receivePump; - CancellationTokenSource receivePumpCancellationTokenSource; + + // Signals that the entire processing should be immediately cancelled because the pump is getting disposed + CancellationTokenSource pumpCancellationTokenSource; + + // Signals that message reception should stop but active processing should continue + CancellationTokenSource receiveCancellationTokenSource; /// /// Creates a new MessageReceiver from a . @@ -899,6 +903,27 @@ public void RegisterMessageHandler(Func handle this.OnMessageHandler(messageHandlerOptions, handler); } + /// + /// Cancels the continuuous reception of messages without closing the underlying service bus connection and unregisters the message handler. + /// Register a message handler first, using + /// or . + /// Although the message handler is unregistered, active threads are not cancelled. + /// + public void UnregisterMessageHandler() + { + this.ThrowIfClosed(); + + lock (this.messageReceivePumpSyncLock) + { + if (this.receivePump != null) + { + this.receiveCancellationTokenSource.Cancel(); + this.receiveCancellationTokenSource.Dispose(); + this.receivePump = null; + } + } + } + /// /// Registers a to be used with this receiver. /// @@ -1001,8 +1026,9 @@ protected override async Task OnClosingAsync() { if (this.receivePump != null) { - this.receivePumpCancellationTokenSource.Cancel(); - this.receivePumpCancellationTokenSource.Dispose(); + this.pumpCancellationTokenSource.Cancel(); + this.pumpCancellationTokenSource.Dispose(); + this.receiveCancellationTokenSource.Dispose(); this.receivePump = null; } } @@ -1276,9 +1302,15 @@ protected virtual void OnMessageHandler( { throw new InvalidOperationException(Resources.MessageHandlerAlreadyRegistered); } + + // pump cancellation token source can be reused on message handlers + if (this.pumpCancellationTokenSource == null) + { + this.pumpCancellationTokenSource = new CancellationTokenSource(); + } - this.receivePumpCancellationTokenSource = new CancellationTokenSource(); - this.receivePump = new MessageReceivePump(this, registerHandlerOptions, callback, this.ServiceBusConnection.Endpoint, this.receivePumpCancellationTokenSource.Token); + this.receiveCancellationTokenSource = new CancellationTokenSource(); + this.receivePump = new MessageReceivePump(this, registerHandlerOptions, callback, this.ServiceBusConnection.Endpoint, this.pumpCancellationTokenSource.Token, this.receiveCancellationTokenSource.Token); } try @@ -1292,8 +1324,9 @@ protected virtual void OnMessageHandler( { if (this.receivePump != null) { - this.receivePumpCancellationTokenSource.Cancel(); - this.receivePumpCancellationTokenSource.Dispose(); + this.pumpCancellationTokenSource.Cancel(); + this.pumpCancellationTokenSource.Dispose(); + this.receiveCancellationTokenSource.Dispose(); this.receivePump = null; } } diff --git a/sdk/servicebus/Microsoft.Azure.ServiceBus/src/MessageReceivePump.cs b/sdk/servicebus/Microsoft.Azure.ServiceBus/src/MessageReceivePump.cs index fa517e95844e3..fb582de3213e6 100644 --- a/sdk/servicebus/Microsoft.Azure.ServiceBus/src/MessageReceivePump.cs +++ b/sdk/servicebus/Microsoft.Azure.ServiceBus/src/MessageReceivePump.cs @@ -16,7 +16,12 @@ sealed class MessageReceivePump readonly string endpoint; readonly MessageHandlerOptions registerHandlerOptions; readonly IMessageReceiver messageReceiver; + + // Signals that the entire processing should be immediately cancelled because the pump is getting disposed readonly CancellationToken pumpCancellationToken; + + // Signals that message reception should stop but active processing should continue + readonly CancellationToken receiveCancellationToken; readonly SemaphoreSlim maxConcurrentCallsSemaphoreSlim; readonly ServiceBusDiagnosticSource diagnosticSource; @@ -24,13 +29,15 @@ public MessageReceivePump(IMessageReceiver messageReceiver, MessageHandlerOptions registerHandlerOptions, Func callback, Uri endpoint, - CancellationToken pumpCancellationToken) + CancellationToken pumpCancellationToken, + CancellationToken receiveCancellationToken) { this.messageReceiver = messageReceiver ?? throw new ArgumentNullException(nameof(messageReceiver)); this.registerHandlerOptions = registerHandlerOptions; this.onMessageCallback = callback; this.endpoint = endpoint.Authority; this.pumpCancellationToken = pumpCancellationToken; + this.receiveCancellationToken = receiveCancellationToken; this.maxConcurrentCallsSemaphoreSlim = new SemaphoreSlim(this.registerHandlerOptions.MaxConcurrentCalls); this.diagnosticSource = new ServiceBusDiagnosticSource(messageReceiver.Path, endpoint); } @@ -55,7 +62,7 @@ Task RaiseExceptionReceived(Exception e, string action) async Task MessagePumpTaskAsync() { - while (!this.pumpCancellationToken.IsCancellationRequested) + while (!this.pumpCancellationToken.IsCancellationRequested && ! this.receiveCancellationToken.IsCancellationRequested) { Message message = null; try diff --git a/sdk/servicebus/Microsoft.Azure.ServiceBus/src/QueueClient.cs b/sdk/servicebus/Microsoft.Azure.ServiceBus/src/QueueClient.cs index 145f5851db851..c481656d31331 100644 --- a/sdk/servicebus/Microsoft.Azure.ServiceBus/src/QueueClient.cs +++ b/sdk/servicebus/Microsoft.Azure.ServiceBus/src/QueueClient.cs @@ -445,6 +445,16 @@ public void RegisterMessageHandler(Func handle this.InnerReceiver.RegisterMessageHandler(handler, messageHandlerOptions); } + /// + /// Cancels the continuuous reception of messages without closing the underlying service bus connection and unregisters the message handler. + /// Register a message handler first, using + /// or + /// + public void UnregisterMessageHandler() + { + this.InnerReceiver.UnregisterMessageHandler(); + } + /// /// Receive session messages continuously from the queue. Registers a message handler and begins a new thread to receive session-messages. /// This handler() is awaited on every time a new message is received by the queue client. diff --git a/sdk/servicebus/Microsoft.Azure.ServiceBus/src/SubscriptionClient.cs b/sdk/servicebus/Microsoft.Azure.ServiceBus/src/SubscriptionClient.cs index c5d7ebeecfc1a..fcd926e72054b 100644 --- a/sdk/servicebus/Microsoft.Azure.ServiceBus/src/SubscriptionClient.cs +++ b/sdk/servicebus/Microsoft.Azure.ServiceBus/src/SubscriptionClient.cs @@ -419,6 +419,16 @@ public void RegisterMessageHandler(Func handle this.InnerSubscriptionClient.InnerReceiver.RegisterMessageHandler(handler, messageHandlerOptions); } + /// + /// Cancels the continuuous reception of messages without closing the underlying service bus connection and unregisters the message handler. + /// Register a message handler first, using + /// or + /// + public void UnregisterMessageHandler() + { + this.InnerSubscriptionClient.InnerReceiver.UnregisterMessageHandler(); + } + /// /// Receive session messages continuously from the queue. Registers a message handler and begins a new thread to receive session-messages. /// This handler() is awaited on every time a new message is received by the subscription client. diff --git a/sdk/servicebus/Microsoft.Azure.ServiceBus/tests/API/ApiApprovals.ApproveAzureServiceBus.approved.txt b/sdk/servicebus/Microsoft.Azure.ServiceBus/tests/API/ApiApprovals.ApproveAzureServiceBus.approved.txt index 18b067f6dda07..4416c48482187 100644 --- a/sdk/servicebus/Microsoft.Azure.ServiceBus/tests/API/ApiApprovals.ApproveAzureServiceBus.approved.txt +++ b/sdk/servicebus/Microsoft.Azure.ServiceBus/tests/API/ApiApprovals.ApproveAzureServiceBus.approved.txt @@ -240,6 +240,7 @@ namespace Microsoft.Azure.ServiceBus public System.Threading.Tasks.Task ScheduleMessageAsync(Microsoft.Azure.ServiceBus.Message message, System.DateTimeOffset scheduleEnqueueTimeUtc) { } public System.Threading.Tasks.Task SendAsync(Microsoft.Azure.ServiceBus.Message message) { } public System.Threading.Tasks.Task SendAsync(System.Collections.Generic.IList messageList) { } + public void UnregisterMessageHandler() { } public override void UnregisterPlugin(string serviceBusPluginName) { } } public sealed class QuotaExceededException : Microsoft.Azure.ServiceBus.ServiceBusException @@ -447,6 +448,7 @@ namespace Microsoft.Azure.ServiceBus public void RegisterSessionHandler(System.Func handler, System.Func exceptionReceivedHandler) { } public void RegisterSessionHandler(System.Func handler, Microsoft.Azure.ServiceBus.SessionHandlerOptions sessionHandlerOptions) { } public System.Threading.Tasks.Task RemoveRuleAsync(string ruleName) { } + public void UnregisterMessageHandler() { } public override void UnregisterPlugin(string serviceBusPluginName) { } } public class TopicClient : Microsoft.Azure.ServiceBus.ClientEntity, Microsoft.Azure.ServiceBus.Core.ISenderClient, Microsoft.Azure.ServiceBus.IClientEntity, Microsoft.Azure.ServiceBus.ITopicClient @@ -520,6 +522,7 @@ namespace Microsoft.Azure.ServiceBus.Core System.Threading.Tasks.Task DeadLetterAsync(string lockToken, string deadLetterReason, string deadLetterErrorDescription = null); void RegisterMessageHandler(System.Func handler, System.Func exceptionReceivedHandler); void RegisterMessageHandler(System.Func handler, Microsoft.Azure.ServiceBus.MessageHandlerOptions messageHandlerOptions); + void UnregisterMessageHandler(); } public interface ISenderClient : Microsoft.Azure.ServiceBus.IClientEntity { @@ -572,6 +575,7 @@ namespace Microsoft.Azure.ServiceBus.Core public override void RegisterPlugin(Microsoft.Azure.ServiceBus.Core.ServiceBusPlugin serviceBusPlugin) { } public System.Threading.Tasks.Task RenewLockAsync(Microsoft.Azure.ServiceBus.Message message) { } public System.Threading.Tasks.Task RenewLockAsync(string lockToken) { } + public void UnregisterMessageHandler() { } public override void UnregisterPlugin(string serviceBusPluginName) { } } public class MessageSender : Microsoft.Azure.ServiceBus.ClientEntity, Microsoft.Azure.ServiceBus.Core.IMessageSender, Microsoft.Azure.ServiceBus.Core.ISenderClient, Microsoft.Azure.ServiceBus.IClientEntity diff --git a/sdk/servicebus/Microsoft.Azure.ServiceBus/tests/MessageReceiverTests.cs b/sdk/servicebus/Microsoft.Azure.ServiceBus/tests/MessageReceiverTests.cs new file mode 100644 index 0000000000000..aca0f94056674 --- /dev/null +++ b/sdk/servicebus/Microsoft.Azure.ServiceBus/tests/MessageReceiverTests.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Azure.ServiceBus.UnitTests +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using Microsoft.Azure.ServiceBus.Core; + using Xunit; + + public sealed class MessageReceiverTests : SenderReceiverClientTestBase + { + public static IEnumerable TestPermutations => new object[][] + { + // Expected structure: { usePartitionedTopic, useSessionTopic, maxCurrentCalls } + new object[] { false, false, 1 }, + new object[] { false, false, 5 }, + new object[] { true, false, 1 }, + new object[] { true, false, 5 }, + }; + + [Theory] + [MemberData(nameof(TestPermutations))] + [LiveTest] + [DisplayTestMethodName] + public Task UnregisterMessageHandlerPeekLockWithAutoCompleteTrue(bool partitioned, bool sessionEnabled, int maxConcurrentCalls) + { + return this.UnregisterMessageHandlerAsync(partitioned, sessionEnabled, maxConcurrentCalls, ReceiveMode.PeekLock, true); + } + + [Theory] + [MemberData(nameof(TestPermutations))] + [LiveTest] + [DisplayTestMethodName] + public Task UnregisterMessageHandlerReceiveDelete(bool partitioned, bool sessionEnabled, int maxConcurrentCalls) + { + return this.UnregisterMessageHandlerAsync(partitioned, sessionEnabled, maxConcurrentCalls, ReceiveMode.ReceiveAndDelete, false); + } + + private async Task UnregisterMessageHandlerAsync(bool partitioned, bool sessionEnabled, int maxConcurrentCalls, ReceiveMode mode, bool autoComplete) + { + const int messageCount = 10; + + await ServiceBusScope.UsingTopicAsync(partitioned, sessionEnabled, async (topicName, subscriptionName) => + { + var topicClient = new TopicClient(TestUtility.NamespaceConnectionString, topicName); + var subscriptionClient = new SubscriptionClient( + TestUtility.NamespaceConnectionString, + topicName, + subscriptionName, + mode); + + try + { + await this.MessageHandlerUnregisterAsyncTestCase( + topicClient.InnerSender, + subscriptionClient.InnerSubscriptionClient.InnerReceiver, + maxConcurrentCalls, + autoComplete, + messageCount); + } + finally + { + await subscriptionClient.CloseAsync(); + await topicClient.CloseAsync(); + } + }); + } + + + [Theory] + [MemberData(nameof(TestPermutations))] + [LiveTest] + [DisplayTestMethodName] + public Task MultipleMessageHandlersPeekLockWithAutoCompleteTrue(bool partitioned, bool sessionEnabled, int maxConcurrentCalls) + { + return this.MultipleMessageHandlersAsync(partitioned, sessionEnabled, maxConcurrentCalls, ReceiveMode.PeekLock, true); + } + + [Theory] + [MemberData(nameof(TestPermutations))] + [LiveTest] + [DisplayTestMethodName] + public Task MultipleMessageHandlersReceiveDelete(bool partitioned, bool sessionEnabled, int maxConcurrentCalls) + { + return this.MultipleMessageHandlersAsync(partitioned, sessionEnabled, maxConcurrentCalls, ReceiveMode.ReceiveAndDelete, false); + } + + private async Task MultipleMessageHandlersAsync(bool partitioned, bool sessionEnabled, int maxConcurrentCalls, ReceiveMode mode, bool autoComplete) + { + const int messageCount = 10; + + await ServiceBusScope.UsingTopicAsync(partitioned, sessionEnabled, async (topicName, subscriptionName) => + { + var topicClient = new TopicClient(TestUtility.NamespaceConnectionString, topicName); + var subscriptionClient = new SubscriptionClient( + TestUtility.NamespaceConnectionString, + topicName, + subscriptionName, + mode); + + try + { + await this.MultipleMessageHandlerAsyncTestCase( + topicClient.InnerSender, + subscriptionClient.InnerSubscriptionClient.InnerReceiver, + maxConcurrentCalls, + autoComplete, + messageCount); + } + finally + { + await subscriptionClient.CloseAsync(); + await topicClient.CloseAsync(); + } + }); + } + } +} \ No newline at end of file diff --git a/sdk/servicebus/Microsoft.Azure.ServiceBus/tests/SenderReceiverClientTestBase.cs b/sdk/servicebus/Microsoft.Azure.ServiceBus/tests/SenderReceiverClientTestBase.cs index 2430b0d7b18c0..a96a6e9b5995f 100644 --- a/sdk/servicebus/Microsoft.Azure.ServiceBus/tests/SenderReceiverClientTestBase.cs +++ b/sdk/servicebus/Microsoft.Azure.ServiceBus/tests/SenderReceiverClientTestBase.cs @@ -275,6 +275,103 @@ internal async Task OnMessageAsyncTestCase( Assert.True(count == messageCount); } + internal async Task MessageHandlerUnregisterAsyncTestCase( + IMessageSender messageSender, + IMessageReceiver messageReceiver, + int maxConcurrentCalls, + bool autoComplete, + int messageCount) + { + var count = 0; + await TestUtility.SendMessagesAsync(messageSender, messageCount); + messageReceiver.RegisterMessageHandler( + async (message, token) => + { + TestUtility.Log($"Received message: SequenceNumber: {message.SystemProperties.SequenceNumber}"); + + // Method should be thread safe and stop the message handler after first batch of messages + messageReceiver.UnregisterMessageHandler(); + + // Simulate "long running" task + Thread.Sleep(1000); + + if (messageReceiver.ReceiveMode == ReceiveMode.PeekLock && !autoComplete) + { + // Completion should still work, even after message handler is stopped + await messageReceiver.CompleteAsync(message.SystemProperties.LockToken); + } + + Interlocked.Increment(ref count); + }, + new MessageHandlerOptions(ExceptionReceivedHandler) { MaxConcurrentCalls = maxConcurrentCalls, AutoComplete = autoComplete }); + + // Wait for the OnMessage Tasks to finish + var stopwatch = Stopwatch.StartNew(); + while (stopwatch.Elapsed.TotalSeconds <= 30) + { + if (count == messageCount) + { + TestUtility.Log($"Too many ('{maxConcurrentCalls}') messages received."); + break; + } + await Task.Delay(TimeSpan.FromSeconds(5)); + } + + Assert.True(count < messageCount); + } + + internal async Task MultipleMessageHandlerAsyncTestCase( + IMessageSender messageSender, + IMessageReceiver messageReceiver, + int maxConcurrentCalls, + bool autoComplete, + int messageCount) + { + var count = 0; + + void HandlerRegistration(bool unregisterImmediately) + { + messageReceiver.RegisterMessageHandler( + async (message, token) => + { + TestUtility.Log($"Received message: SequenceNumber: {message.SystemProperties.SequenceNumber}"); + + if (unregisterImmediately) + { + messageReceiver.UnregisterMessageHandler(); + + // recursively register a new handler and expect it to handle the open messages + HandlerRegistration(false); + } + + if (messageReceiver.ReceiveMode == ReceiveMode.PeekLock && !autoComplete) + { + // Completion should still work, even after message handler is stopped + await messageReceiver.CompleteAsync(message.SystemProperties.LockToken); + } + + Interlocked.Increment(ref count); + }, + new MessageHandlerOptions(ExceptionReceivedHandler) { MaxConcurrentCalls = maxConcurrentCalls, AutoComplete = autoComplete }); + } + + await TestUtility.SendMessagesAsync(messageSender, messageCount); + HandlerRegistration(true); + + // Wait for the OnMessage Tasks to finish + var stopwatch = Stopwatch.StartNew(); + while (stopwatch.Elapsed.TotalSeconds <= 60) + { + if (count == messageCount) + { + TestUtility.Log($"All '{messageCount}' messages Received."); + break; + } + await Task.Delay(TimeSpan.FromSeconds(5)); + } + Assert.True(count == messageCount); + } + internal async Task OnMessageRegistrationWithoutPendingMessagesTestCase( IMessageSender messageSender, IMessageReceiver messageReceiver, From 18e0a41d3c6488b0629fa45532b9eaccd635053b Mon Sep 17 00:00:00 2001 From: Peter Rang Date: Wed, 5 Jun 2019 10:24:38 +0200 Subject: [PATCH 2/3] Set up CI with Azure Pipelines [skip ci] --- azure-pipelines.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 azure-pipelines.yml diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000000000..cc829ca3e7982 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,34 @@ +# ASP.NET Core (.NET Framework) +# Build and test ASP.NET Core projects targeting the full .NET Framework. +# Add steps that publish symbols, save build artifacts, and more: +# https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core + +trigger: +- master + +pool: + vmImage: 'windows-latest' + +variables: + solution: '**/*.sln' + buildPlatform: 'Any CPU' + buildConfiguration: 'Release' + +steps: +- task: NuGetToolInstaller@0 + +- task: NuGetCommand@2 + inputs: + restoreSolution: '$(solution)' + +- task: VSBuild@1 + inputs: + solution: '$(solution)' + msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:DesktopBuildPackageLocation="$(build.artifactStagingDirectory)\WebApp.zip" /p:DeployIisAppPath="Default Web Site"' + platform: '$(buildPlatform)' + configuration: '$(buildConfiguration)' + +- task: VSTest@2 + inputs: + platform: '$(buildPlatform)' + configuration: '$(buildConfiguration)' From 8637512267ba01c5c128a615626226a2227ed9f5 Mon Sep 17 00:00:00 2001 From: Peter Rang Date: Wed, 5 Jun 2019 10:28:23 +0200 Subject: [PATCH 3/3] Delete azure-pipelines.yml --- azure-pipelines.yml | 34 ---------------------------------- 1 file changed, 34 deletions(-) delete mode 100644 azure-pipelines.yml diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index cc829ca3e7982..0000000000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,34 +0,0 @@ -# ASP.NET Core (.NET Framework) -# Build and test ASP.NET Core projects targeting the full .NET Framework. -# Add steps that publish symbols, save build artifacts, and more: -# https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core - -trigger: -- master - -pool: - vmImage: 'windows-latest' - -variables: - solution: '**/*.sln' - buildPlatform: 'Any CPU' - buildConfiguration: 'Release' - -steps: -- task: NuGetToolInstaller@0 - -- task: NuGetCommand@2 - inputs: - restoreSolution: '$(solution)' - -- task: VSBuild@1 - inputs: - solution: '$(solution)' - msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:DesktopBuildPackageLocation="$(build.artifactStagingDirectory)\WebApp.zip" /p:DeployIisAppPath="Default Web Site"' - platform: '$(buildPlatform)' - configuration: '$(buildConfiguration)' - -- task: VSTest@2 - inputs: - platform: '$(buildPlatform)' - configuration: '$(buildConfiguration)'