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"),
+ ]);
}