diff --git a/src/Analyzers/KnownTypeSymbols.Net.cs b/src/Analyzers/KnownTypeSymbols.Net.cs index c9bde3ed..8e327319 100644 --- a/src/Analyzers/KnownTypeSymbols.Net.cs +++ b/src/Analyzers/KnownTypeSymbols.Net.cs @@ -23,6 +23,7 @@ public sealed partial class KnownTypeSymbols INamedTypeSymbol? cancellationToken; INamedTypeSymbol? environment; INamedTypeSymbol? httpClient; + INamedTypeSymbol? timeProvider; INamedTypeSymbol? iLogger; /// @@ -81,4 +82,9 @@ public sealed partial class KnownTypeSymbols /// Gets an ILogger type symbol. /// public INamedTypeSymbol? ILogger => this.GetOrResolveFullyQualifiedType("Microsoft.Extensions.Logging.ILogger", ref this.iLogger); + + /// + /// Gets a TimeProvider type symbol. + /// + public INamedTypeSymbol? TimeProvider => this.GetOrResolveFullyQualifiedType("System.TimeProvider", ref this.timeProvider); } diff --git a/src/Analyzers/Orchestration/DateTimeOrchestrationAnalyzer.cs b/src/Analyzers/Orchestration/DateTimeOrchestrationAnalyzer.cs index ccfc81ed..97c8241f 100644 --- a/src/Analyzers/Orchestration/DateTimeOrchestrationAnalyzer.cs +++ b/src/Analyzers/Orchestration/DateTimeOrchestrationAnalyzer.cs @@ -10,7 +10,7 @@ namespace Microsoft.DurableTask.Analyzers.Orchestration; /// -/// Analyzer that reports a warning when a non-deterministic DateTime or DateTimeOffset property is used in an orchestration method. +/// Analyzer that reports a warning when a non-deterministic DateTime, DateTimeOffset, or TimeProvider method is used in an orchestration method. /// [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class DateTimeOrchestrationAnalyzer : OrchestrationAnalyzer @@ -36,18 +36,20 @@ public sealed class DateTimeOrchestrationAnalyzer : OrchestrationAnalyzer SupportedDiagnostics => [Rule]; /// - /// Visitor that inspects the method body for DateTime and DateTimeOffset properties. + /// Visitor that inspects the method body for DateTime and DateTimeOffset properties, and TimeProvider method invocations. /// public sealed class DateTimeOrchestrationVisitor : MethodProbeOrchestrationVisitor { INamedTypeSymbol systemDateTimeSymbol = null!; INamedTypeSymbol? systemDateTimeOffsetSymbol; + INamedTypeSymbol? systemTimeProviderSymbol; /// public override bool Initialize() { this.systemDateTimeSymbol = this.Compilation.GetSpecialType(SpecialType.System_DateTime); this.systemDateTimeOffsetSymbol = this.Compilation.GetTypeByMetadataName("System.DateTimeOffset"); + this.systemTimeProviderSymbol = this.Compilation.GetTypeByMetadataName("System.TimeProvider"); return true; } @@ -86,6 +88,33 @@ protected override void VisitMethod(SemanticModel semanticModel, SyntaxNode meth reportDiagnostic(RoslynExtensions.BuildDiagnostic(Rule, operation.Syntax, methodSymbol.Name, property.ToString(), orchestrationName)); } } + + // Check for TimeProvider method invocations + if (this.systemTimeProviderSymbol is not null) + { + foreach (IInvocationOperation operation in methodOperation.Descendants().OfType()) + { + IMethodSymbol invokedMethod = operation.TargetMethod; + + // Check if the method is called on TimeProvider type + bool isTimeProvider = invokedMethod.ContainingType.Equals(this.systemTimeProviderSymbol, SymbolEqualityComparer.Default); + + if (!isTimeProvider) + { + continue; + } + + // Check for non-deterministic TimeProvider methods: GetUtcNow, GetLocalNow, GetTimestamp + bool isNonDeterministicMethod = invokedMethod.Name is "GetUtcNow" or "GetLocalNow" or "GetTimestamp"; + + if (isNonDeterministicMethod) + { + // e.g.: "The method 'Method1' uses 'System.TimeProvider.GetUtcNow()' that may cause non-deterministic behavior when invoked from orchestration 'MyOrchestrator'" + string timeProviderMethodName = $"{invokedMethod.ContainingType}.{invokedMethod.Name}()"; + reportDiagnostic(RoslynExtensions.BuildDiagnostic(Rule, operation.Syntax, methodSymbol.Name, timeProviderMethodName, orchestrationName)); + } + } + } } } } diff --git a/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs b/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs index fd233d1b..221d6048 100644 --- a/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs +++ b/src/Analyzers/Orchestration/DateTimeOrchestrationFixer.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.Composition; using System.Globalization; +using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; @@ -26,51 +27,89 @@ public sealed class DateTimeOrchestrationFixer : OrchestrationContextFixer /// protected override void RegisterCodeFixes(CodeFixContext context, OrchestrationCodeFixContext orchestrationContext) { - // Parses the syntax node to see if it is a member access expression (e.g. DateTime.Now or DateTimeOffset.Now) - if (orchestrationContext.SyntaxNodeWithDiagnostic is not MemberAccessExpressionSyntax dateTimeExpression) - { - return; - } - // Gets the name of the TaskOrchestrationContext parameter (e.g. "context" or "ctx") string contextParameterName = orchestrationContext.TaskOrchestrationContextSymbol.Name; - - // Use semantic analysis to determine if this is a DateTimeOffset expression SemanticModel semanticModel = orchestrationContext.SemanticModel; - ITypeSymbol? typeSymbol = semanticModel.GetTypeInfo(dateTimeExpression.Expression).Type; - bool isDateTimeOffset = typeSymbol?.ToDisplayString() == "System.DateTimeOffset"; - bool isDateTimeToday = dateTimeExpression.Name.ToString() == "Today"; - - // Build the recommendation text - string recommendation; - if (isDateTimeOffset) + // Handle DateTime/DateTimeOffset property access (e.g. DateTime.Now or DateTimeOffset.Now) + if (orchestrationContext.SyntaxNodeWithDiagnostic is MemberAccessExpressionSyntax dateTimeExpression) { - // For DateTimeOffset, we always just cast CurrentUtcDateTime - recommendation = $"(DateTimeOffset){contextParameterName}.CurrentUtcDateTime"; + // Use semantic analysis to determine if this is a DateTimeOffset expression + ITypeSymbol? typeSymbol = semanticModel.GetTypeInfo(dateTimeExpression.Expression).Type; + bool isDateTimeOffset = typeSymbol?.ToDisplayString() == "System.DateTimeOffset"; + + bool isDateTimeToday = dateTimeExpression.Name.ToString() == "Today"; + + // Build the recommendation text + string recommendation; + if (isDateTimeOffset) + { + // For DateTimeOffset, we always just cast CurrentUtcDateTime + recommendation = $"(DateTimeOffset){contextParameterName}.CurrentUtcDateTime"; + } + else + { + // For DateTime, we may need to add .Date for Today + string dateTimeTodaySuffix = isDateTimeToday ? ".Date" : string.Empty; + recommendation = $"{contextParameterName}.CurrentUtcDateTime{dateTimeTodaySuffix}"; + } + + // e.g: "Use 'context.CurrentUtcDateTime' instead of 'DateTime.Now'" + // e.g: "Use 'context.CurrentUtcDateTime.Date' instead of 'DateTime.Today'" + // e.g: "Use '(DateTimeOffset)context.CurrentUtcDateTime' instead of 'DateTimeOffset.Now'" + string title = string.Format( + CultureInfo.InvariantCulture, + Resources.UseInsteadFixerTitle, + recommendation, + dateTimeExpression); + + context.RegisterCodeFix( + CodeAction.Create( + title: title, + createChangedDocument: c => ReplaceDateTime(context.Document, orchestrationContext.Root, dateTimeExpression, contextParameterName, isDateTimeToday, isDateTimeOffset), + equivalenceKey: title), // This key is used to prevent duplicate code fixes. + context.Diagnostics); + return; } - else + + // Handle TimeProvider method invocations (e.g. TimeProvider.System.GetUtcNow()) + // The node might be the invocation itself or a child node, so we need to find the InvocationExpressionSyntax + InvocationExpressionSyntax? timeProviderInvocation = orchestrationContext.SyntaxNodeWithDiagnostic as InvocationExpressionSyntax + ?? orchestrationContext.SyntaxNodeWithDiagnostic.AncestorsAndSelf().OfType().FirstOrDefault(); + + if (timeProviderInvocation != null && + semanticModel.GetSymbolInfo(timeProviderInvocation).Symbol is IMethodSymbol methodSymbol) { - // For DateTime, we may need to add .Date for Today - string dateTimeTodaySuffix = isDateTimeToday ? ".Date" : string.Empty; - recommendation = $"{contextParameterName}.CurrentUtcDateTime{dateTimeTodaySuffix}"; - } + string methodName = methodSymbol.Name; + + // Check if the method returns DateTimeOffset + bool returnsDateTimeOffset = methodSymbol.ReturnType.ToDisplayString() == "System.DateTimeOffset"; + + // Build the recommendation based on the method name + string recommendation = methodName switch + { + "GetUtcNow" when returnsDateTimeOffset => $"(DateTimeOffset){contextParameterName}.CurrentUtcDateTime", + "GetUtcNow" => $"{contextParameterName}.CurrentUtcDateTime", + "GetLocalNow" when returnsDateTimeOffset => $"(DateTimeOffset){contextParameterName}.CurrentUtcDateTime.ToLocalTime()", + "GetLocalNow" => $"{contextParameterName}.CurrentUtcDateTime.ToLocalTime()", + "GetTimestamp" => $"{contextParameterName}.CurrentUtcDateTime.Ticks", + _ => $"{contextParameterName}.CurrentUtcDateTime", + }; + + // e.g: "Use 'context.CurrentUtcDateTime' instead of 'TimeProvider.System.GetUtcNow()'" + string title = string.Format( + CultureInfo.InvariantCulture, + Resources.UseInsteadFixerTitle, + recommendation, + timeProviderInvocation); - // e.g: "Use 'context.CurrentUtcDateTime' instead of 'DateTime.Now'" - // e.g: "Use 'context.CurrentUtcDateTime.Date' instead of 'DateTime.Today'" - // e.g: "Use '(DateTimeOffset)context.CurrentUtcDateTime' instead of 'DateTimeOffset.Now'" - string title = string.Format( - CultureInfo.InvariantCulture, - Resources.UseInsteadFixerTitle, - recommendation, - dateTimeExpression.ToString()); - - context.RegisterCodeFix( - CodeAction.Create( - title: title, - createChangedDocument: c => ReplaceDateTime(context.Document, orchestrationContext.Root, dateTimeExpression, contextParameterName, isDateTimeToday, isDateTimeOffset), - equivalenceKey: title), // This key is used to prevent duplicate code fixes. - context.Diagnostics); + context.RegisterCodeFix( + CodeAction.Create( + title: title, + createChangedDocument: c => ReplaceTimeProvider(context.Document, orchestrationContext.Root, timeProviderInvocation, contextParameterName, methodName, returnsDateTimeOffset), + equivalenceKey: title), + context.Diagnostics); + } } static Task ReplaceDateTime(Document document, SyntaxNode oldRoot, MemberAccessExpressionSyntax incorrectDateTimeSyntax, string contextParameterName, bool isDateTimeToday, bool isDateTimeOffset) @@ -106,4 +145,49 @@ static Task ReplaceDateTime(Document document, SyntaxNode oldRoot, Mem return Task.FromResult(newDocument); } + + static Task ReplaceTimeProvider(Document document, SyntaxNode oldRoot, InvocationExpressionSyntax incorrectTimeProviderSyntax, string contextParameterName, string methodName, bool returnsDateTimeOffset) + { + // Build the correct expression based on the method name + ExpressionSyntax correctExpression = methodName switch + { + "GetUtcNow" => MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(contextParameterName), + IdentifierName("CurrentUtcDateTime")), + "GetLocalNow" => InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(contextParameterName), + IdentifierName("CurrentUtcDateTime")), + IdentifierName("ToLocalTime"))), + "GetTimestamp" => MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(contextParameterName), + IdentifierName("CurrentUtcDateTime")), + IdentifierName("Ticks")), + _ => MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(contextParameterName), + IdentifierName("CurrentUtcDateTime")), + }; + + // If the method returns DateTimeOffset, we need to cast the DateTime to DateTimeOffset + if (returnsDateTimeOffset) + { + correctExpression = CastExpression( + IdentifierName("DateTimeOffset"), + correctExpression); + } + + // Replaces the old invocation with the new expression + SyntaxNode newRoot = oldRoot.ReplaceNode(incorrectTimeProviderSyntax, correctExpression); + Document newDocument = document.WithSyntaxRoot(newRoot); + + return Task.FromResult(newDocument); + } } diff --git a/test/Analyzers.Tests/Orchestration/DateTimeOrchestrationAnalyzerTests.cs b/test/Analyzers.Tests/Orchestration/DateTimeOrchestrationAnalyzerTests.cs index 4404dff8..e42fa379 100644 --- a/test/Analyzers.Tests/Orchestration/DateTimeOrchestrationAnalyzerTests.cs +++ b/test/Analyzers.Tests/Orchestration/DateTimeOrchestrationAnalyzerTests.cs @@ -453,56 +453,128 @@ public async Task FuncOrchestratorWithDateTimeOffsetHasDiag() await VerifyCS.VerifyDurableTaskCodeFixAsync(code, expected, fix); } + [Theory] + [InlineData("TimeProvider.System.GetUtcNow()")] + [InlineData("TimeProvider.System.GetLocalNow()")] + public async Task DurableFunctionOrchestrationUsingTimeProviderNonDeterministicMethodsHasDiag(string expression) + { + string code = Wrapper.WrapDurableFunctionOrchestration($@" +[Function(""Run"")] +DateTimeOffset Run([OrchestrationTrigger] TaskOrchestrationContext context) +{{ + return {{|#0:{expression}|}}; +}} +"); + + string expectedReplacement = expression.Contains("GetLocalNow") + ? "(DateTimeOffset)context.CurrentUtcDateTime.ToLocalTime()" + : "(DateTimeOffset)context.CurrentUtcDateTime"; + + string fix = Wrapper.WrapDurableFunctionOrchestration($@" +[Function(""Run"")] +DateTimeOffset Run([OrchestrationTrigger] TaskOrchestrationContext context) +{{ + return {expectedReplacement}; +}} +"); + + // The analyzer reports the method name as "System.TimeProvider.GetUtcNow()" or "System.TimeProvider.GetLocalNow()" + string methodName = expression.Contains("GetLocalNow") ? "System.TimeProvider.GetLocalNow()" : "System.TimeProvider.GetUtcNow()"; + DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Run", methodName, "Run"); + + await VerifyCS.VerifyNet80CodeFixAsync(code, expected, fix); + } + [Fact] - public async Task TaskOrchestratorSdkOnlyHasDiag() + public async Task DurableFunctionOrchestrationUsingTimeProviderGetTimestampHasDiag() { - // Tests that the analyzer works with SDK-only references (without Azure Functions assemblies) - string code = Wrapper.WrapTaskOrchestratorSdkOnly(@" -public class MyOrchestrator : TaskOrchestrator + string code = Wrapper.WrapDurableFunctionOrchestration(@" +[Function(""Run"")] +long Run([OrchestrationTrigger] TaskOrchestrationContext context) { - public override Task RunAsync(TaskOrchestrationContext context, string input) + return {|#0:TimeProvider.System.GetTimestamp()|}; +} +"); + + string fix = Wrapper.WrapDurableFunctionOrchestration(@" +[Function(""Run"")] +long Run([OrchestrationTrigger] TaskOrchestrationContext context) +{ + return context.CurrentUtcDateTime.Ticks; +} +"); + + DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Run", "System.TimeProvider.GetTimestamp()", "Run"); + + await VerifyCS.VerifyNet80CodeFixAsync(code, expected, fix); + } + + [Fact] + public async Task TaskOrchestratorUsingTimeProviderHasDiag() { - return Task.FromResult({|#0:DateTime.Now|}); + string code = Wrapper.WrapTaskOrchestrator(@" +public class MyOrchestrator : TaskOrchestrator +{ + public override Task RunAsync(TaskOrchestrationContext context, string input) + { + return Task.FromResult({|#0:TimeProvider.System.GetUtcNow()|}); } } "); - string fix = Wrapper.WrapTaskOrchestratorSdkOnly(@" -public class MyOrchestrator : TaskOrchestrator + string fix = Wrapper.WrapTaskOrchestrator(@" +public class MyOrchestrator : TaskOrchestrator { - public override Task RunAsync(TaskOrchestrationContext context, string input) + public override Task RunAsync(TaskOrchestrationContext context, string input) { - return Task.FromResult(context.CurrentUtcDateTime); + return Task.FromResult((DateTimeOffset)context.CurrentUtcDateTime); } } "); - DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("RunAsync", "System.DateTime.Now", "MyOrchestrator"); + DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("RunAsync", "System.TimeProvider.GetUtcNow()", "MyOrchestrator"); - await VerifyCS.VerifySdkOnlyCodeFixAsync(code, expected, fix); + await VerifyCS.VerifyNet80CodeFixAsync(code, expected, fix); } [Fact] - public async Task FuncOrchestratorSdkOnlyWithLambdaHasDiag() + public async Task FuncOrchestratorWithTimeProviderHasDiag() { - // Tests that the analyzer works with SDK-only references (without Azure Functions assemblies) - string code = Wrapper.WrapFuncOrchestratorSdkOnly(@" + string code = Wrapper.WrapFuncOrchestrator(@" tasks.AddOrchestratorFunc(""HelloSequence"", context => { - return {|#0:DateTime.Now|}; + return {|#0:TimeProvider.System.GetUtcNow()|}; }); "); - string fix = Wrapper.WrapFuncOrchestratorSdkOnly(@" + string fix = Wrapper.WrapFuncOrchestrator(@" tasks.AddOrchestratorFunc(""HelloSequence"", context => { - return context.CurrentUtcDateTime; + return (DateTimeOffset)context.CurrentUtcDateTime; }); "); - DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Main", "System.DateTime.Now", "HelloSequence"); + DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("Main", "System.TimeProvider.GetUtcNow()", "HelloSequence"); + + await VerifyCS.VerifyNet80CodeFixAsync(code, expected, fix); + } + + [Fact] + public async Task DurableFunctionOrchestrationInvokingMethodWithTimeProviderHasDiag() + { + string code = Wrapper.WrapDurableFunctionOrchestration(@" +[Function(""Run"")] +DateTimeOffset Run([OrchestrationTrigger] TaskOrchestrationContext context) +{ + return GetTime(); +} + +DateTimeOffset GetTime() => {|#0:TimeProvider.System.GetUtcNow()|}; +"); + + DiagnosticResult expected = BuildDiagnostic().WithLocation(0).WithArguments("GetTime", "System.TimeProvider.GetUtcNow()", "Run"); - await VerifyCS.VerifySdkOnlyCodeFixAsync(code, expected, fix); + await VerifyCS.VerifyNet80AnalyzerAsync(code, expected); } static DiagnosticResult BuildDiagnostic() diff --git a/test/Analyzers.Tests/Verifiers/CSharpCodeFixVerifier.Durable.cs b/test/Analyzers.Tests/Verifiers/CSharpCodeFixVerifier.Durable.cs index b69fae93..552f7a13 100644 --- a/test/Analyzers.Tests/Verifiers/CSharpCodeFixVerifier.Durable.cs +++ b/test/Analyzers.Tests/Verifiers/CSharpCodeFixVerifier.Durable.cs @@ -1,7 +1,4 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Testing; @@ -89,6 +86,53 @@ public static async Task VerifySdkOnlyCodeFixAsync( References.SdkOnlyAssemblies, configureTest); } + /// + /// Runs analyzer test with .NET 8.0 references for testing APIs only available in .NET 8+. + /// Used for TimeProvider and other .NET 8+ specific tests. + /// + public static Task VerifyNet80AnalyzerAsync(string source, params DiagnosticResult[] expected) + { + return VerifyNet80AnalyzerAsync(source, null, expected); + } + + /// + /// Runs analyzer test with .NET 8.0 references for testing APIs only available in .NET 8+. + /// Used for TimeProvider and other .NET 8+ specific tests. + /// + public static async Task VerifyNet80AnalyzerAsync( + string source, Action? configureTest = null, params DiagnosticResult[] expected) + { + await RunAsync(expected, new Test() + { + TestCode = source, + }, References.Net80Assemblies, configureTest); + } + + /// + /// Runs code fix test with .NET 8.0 references for testing APIs only available in .NET 8+. + /// Used for TimeProvider and other .NET 8+ specific tests. + /// + public static Task VerifyNet80CodeFixAsync( + string source, DiagnosticResult expected, string fixedSource, Action? configureTest = null) + { + return VerifyNet80CodeFixAsync(source, [expected], fixedSource, configureTest); + } + + /// + /// Runs code fix test with .NET 8.0 references for testing APIs only available in .NET 8+. + /// Used for TimeProvider and other .NET 8+ specific tests. + /// + public static async Task VerifyNet80CodeFixAsync( + string source, DiagnosticResult[] expected, string fixedSource, Action? configureTest = null) + { + await RunAsync(expected, new Test() + { + TestCode = source, + FixedCode = fixedSource, + }, + References.Net80Assemblies, configureTest); + } + static async Task RunAsync(DiagnosticResult[] expected, Test test, ReferenceAssemblies referenceAssemblies, Action? configureTest = null) { test.ReferenceAssemblies = referenceAssemblies; diff --git a/test/Analyzers.Tests/Verifiers/References.cs b/test/Analyzers.Tests/Verifiers/References.cs index 59de0dda..701a04cf 100644 --- a/test/Analyzers.Tests/Verifiers/References.cs +++ b/test/Analyzers.Tests/Verifiers/References.cs @@ -9,6 +9,7 @@ public static class References { static readonly Lazy durableAssemblyReferences = new(() => BuildReferenceAssemblies()); static readonly Lazy durableSdkOnlyReferences = new(() => BuildSdkOnlyReferenceAssemblies()); + static readonly Lazy durableNet80References = new(() => BuildNet80ReferenceAssemblies()); public static ReferenceAssemblies CommonAssemblies => durableAssemblyReferences.Value; @@ -18,6 +19,12 @@ public static class References /// public static ReferenceAssemblies SdkOnlyAssemblies => durableSdkOnlyReferences.Value; + /// + /// Gets assembly references targeting .NET 8.0 for testing APIs only available in .NET 8+. + /// Used for TimeProvider and other .NET 8+ specific tests. + /// + public static ReferenceAssemblies Net80Assemblies => durableNet80References.Value; + static ReferenceAssemblies BuildReferenceAssemblies() => ReferenceAssemblies.Net.Net60.AddPackages([ new PackageIdentity("Azure.Storage.Blobs", "12.17.0"), new PackageIdentity("Azure.Storage.Queues", "12.17.0"), @@ -37,4 +44,14 @@ static ReferenceAssemblies BuildSdkOnlyReferenceAssemblies() => ReferenceAssembl new PackageIdentity("Microsoft.DurableTask.Abstractions", "1.3.0"), new PackageIdentity("Microsoft.DurableTask.Worker", "1.3.0"), ]); + + static ReferenceAssemblies BuildNet80ReferenceAssemblies() => ReferenceAssemblies.Net.Net80.AddPackages([ + new PackageIdentity("Azure.Storage.Blobs", "12.17.0"), + new PackageIdentity("Azure.Storage.Queues", "12.17.0"), + new PackageIdentity("Azure.Data.Tables", "12.8.3"), + new PackageIdentity("Microsoft.Azure.Cosmos", "3.39.1"), + new PackageIdentity("Microsoft.Azure.Functions.Worker", "1.21.0"), + new PackageIdentity("Microsoft.Azure.Functions.Worker.Extensions.DurableTask", "1.1.1"), + new PackageIdentity("Microsoft.Data.SqlClient", "5.2.0"), + ]); }